Skip to content

Conversation

@MasterAffan
Copy link

Summary

Fixes #79

The argo_list_experiments tool was accepting a rollout_name parameter but not effectively using it to filter experiments due to an incorrect label selector.

Problem

The original implementation attempted to use a label selector that did not exist or was malformed:

if rollout_name:
    # Filter by rollout name using label selector
    cmd_parts.extend(["-l", f"rollouts-pod-template-hash"]) # Missing value!

The label selector rollouts-pod-template-hash was incomplete—it didn't include the actual rollout name or a valid selector format (key=value).

Solution

After researching Argo Rollouts internals, I found that:

  1. Experiments don't have a guaranteed label referencing the parent rollout name.
  2. Experiments inherit labels from the parent Rollout, but there is no standard rollouts.argoproj.io/rollout label.

However, Argo Rollouts uses a reliable naming convention where experiment names follow the pattern:
{rollout-name}-{podHash}-{revision}-{stepIndex}

The fix implements filtering by experiment name prefix using JSON output parsing:

if rollout_name:
    # Use JSON output to filter by experiment name prefix
    cmd_parts = ["get", "experiments.argoproj.io", "-o", "json"]
    
# ... after execution ...
filtered = [exp for exp in items 
            if exp["metadata"]["name"].startswith(f"{rollout_name}-")]

Changes

mcp/tools/argo.py:

  • Added import json for parsing kubectl output.

  • Modified argo_list_experiments() to:

  • Fetch experiments in JSON format when filtering by rollout name.

  • Filter experiments by checking if their name starts with the rollout name.

  • Format filtered results as a readable table (NAME, STATUS, AGE).

  • Return a clear message when no experiments are found for the rollout.

  • Updated parameter description to explain the filtering mechanism.

  • Added detailed docstring explaining the filtering behavior.

Resources

The rollout_name parameter was accepted but not used in the label selector.
The original code had an incomplete label selector 'rollouts-pod-template-hash'
that was missing the actual rollout name value.

This fix implements filtering by experiment name prefix, leveraging Argo
Rollouts' naming convention where experiment names follow the pattern:
{rollout-name}-{podHash}-{revision}-{stepIndex}.

Changes:
- Add json import for parsing kubectl output
- Fetch experiments in JSON format when filtering by rollout name
- Filter experiments by checking if name starts with rollout name
- Format filtered results as readable table (NAME, STATUS, AGE)
- Return clear message when no experiments found for the rollout
- Update parameter description and docstring

Fixes skyflo-ai#79
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Filter experiments by rollout name and display results in a formatted table (NAME, STATUS, AGE).
    • Show a friendly message when no experiments match the filter.
  • Refactor

    • Switched to server-output processing for more reliable filtering and display; retains previous wide output when no filter is used.
  • Improvements

    • Age displayed in kubectl-style relative format and improved error handling with clear messages on parse failures.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Reworked mcp/tools/argo.py to fix rollout-based filtering in argo_list_experiments: removed the broken kubectl label selector, emit JSON from kubectl when a rollout is specified, and perform client-side filtering by metadata.name prefix with tabular output and robust JSON error handling.

Changes

Cohort / File(s) Summary
Argo experiments filtering refactor
mcp/tools/argo.py
Added json import and _format_age. Changed argo_list_experiments to request JSON when rollout_name is provided, removed the broken kubectl label selector, perform client-side filtering where metadata.name starts with "{rollout_name}-", format results as a NAME / STATUS / AGE table, return "No experiments found for rollout" when none match, and handle JSON decode errors gracefully.

Sequence Diagram(s)

(omitted — change is internal filtering and does not introduce multi-component sequential interactions warranting a diagram)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through JSON, crisp and bright,
I matched rollouts by name just right.
A table sprouted, tidy and neat,
No broken selector, victory sweet! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: restoring the broken rollout_name parameter functionality in argo_list_experiments.
Description check ✅ Passed The description is detailed and directly relevant, explaining the problem, investigation findings, solution approach, and implementation changes.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #79: implements rollout_name filtering via JSON parsing, preserves unfiltered behavior, provides valid alternative to broken label selector, updates parameter documentation, and handles edge cases.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the argo_list_experiments rollout_name filtering; no unrelated modifications or refactoring detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e76da93 and 6a62c6f.

📒 Files selected for processing (1)
  • mcp/tools/argo.py
🧰 Additional context used
🧬 Code graph analysis (1)
mcp/tools/argo.py (1)
mcp/utils/commands.py (1)
  • run_command (8-50)
🔇 Additional comments (4)
mcp/tools/argo.py (4)

3-4: LGTM!

Standard library imports correctly added to support JSON parsing and relative age calculation.


19-36: LGTM!

The _format_age helper correctly converts ISO timestamps to kubectl-style relative ages with proper timezone handling. The fallback to the original timestamp on parse errors is a sensible defensive choice.


259-262: LGTM!

The parameter description clearly documents the filtering mechanism and the Argo Rollouts naming convention used for matching.


270-311: LGTM! Well-structured implementation addressing all review feedback.

The refactored function correctly:

  • Uses JSON output for filtering and wide output for listing all experiments
  • Filters by name prefix with the trailing dash to avoid false matches
  • Formats output with fixed-width columns for proper alignment
  • Converts timestamps to relative ages using _format_age
  • Handles JSON parse errors gracefully with an informative error message

The trailing dash in startswith(f"{rollout_name}-") is a good detail that prevents matching experiments from similarly-named rollouts (e.g., rollout "app" won't match "app2-hash-1-0").

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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)
mcp/tools/argo.py (1)

291-293: Consider logging or returning a clearer error on JSON parse failure.

Silently passing on JSONDecodeError and returning the raw JSON output could confuse users. Consider either logging a warning or returning a more explicit error message.

🔎 Suggested improvement
         except json.JSONDecodeError:
-            # If JSON parsing fails, return original output
-            pass
+            # If JSON parsing fails, return an error message
+            result["output"] = f"Failed to parse experiment data. Raw output: {result['output'][:200]}..."
+            result["error"] = True

Alternatively, if you prefer silent degradation, at minimum add logging for debugging:

+import logging
+logger = logging.getLogger(__name__)
+
         except json.JSONDecodeError as e:
-            # If JSON parsing fails, return original output
-            pass
+            logger.warning(f"Failed to parse experiments JSON: {e}")
+            # Fall through to return original output
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c5a3f8 and e76da93.

📒 Files selected for processing (1)
  • mcp/tools/argo.py
🧰 Additional context used
🧬 Code graph analysis (1)
mcp/tools/argo.py (1)
mcp/utils/commands.py (1)
  • run_command (8-50)
🔇 Additional comments (1)
mcp/tools/argo.py (1)

256-268: Filtering approach looks correct.

The implementation correctly addresses the issue by:

  1. Using JSON output when filtering is needed
  2. Applying client-side prefix matching based on Argo's naming convention
  3. Falling back to wide output when no filter is specified

This is a sensible workaround given Argo Rollouts experiments don't have a reliable label for parent rollout.

Copy link
Member

@KaranJagtiani KaranJagtiani left a comment

Choose a reason for hiding this comment

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

Remove all redundant comments as well.

result = await run_command("kubectl", cmd_parts)

# If filtering by rollout name, parse JSON and filter experiments
if rollout_name and result.get("output") and not result.get("error"):
Copy link
Member

Choose a reason for hiding this comment

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

The function is declared to return ToolOutput, which is a TypedDict with keys output: str and error: bool. However, when rollout_name is provided, the code uses result.get("output") and result.get("error"), then mutates result["output"] directly.

This maintains consistency with the TypedDict contract:

Suggested change
if rollout_name and result.get("output") and not result.get("error"):
if rollout_name and result["output"] and not result["error"]:

Comment on lines 282 to 287
output_lines = ["NAME\t\tSTATUS\t\tAGE"]
for exp in filtered:
name = exp.get("metadata", {}).get("name", "")
status = exp.get("status", {}).get("phase", "Unknown")
created = exp.get("metadata", {}).get("creationTimestamp", "")
output_lines.append(f"{name}\t{status}\t{created}")
Copy link
Member

Choose a reason for hiding this comment

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

The column header says "AGE" but displays creationTimestamp (e.g., 2026-01-05T10:30:00Z). This is inconsistent with kubectl output, which shows relative age (e.g., 5m, 2h, 3d).

CodeRabbit raised this. It must be fixed.

Add a helper function to convert ISO timestamps to relative age:

from datetime import datetime, timezone

def _format_age(timestamp: str) -> str:
    """Convert ISO timestamp to kubectl-style relative age."""
    try:
        created = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
        delta = datetime.now(timezone.utc) - created
        seconds = int(delta.total_seconds())
        if seconds < 0:
            return "0s"
        if seconds < 60:
            return f"{seconds}s"
        if seconds < 3600:
            return f"{seconds // 60}m"
        if seconds < 86400:
            return f"{seconds // 3600}h"
        return f"{seconds // 86400}d"
    except (ValueError, TypeError):
        return timestamp

Then use _format_age(created) instead of created directly.

Comment on lines 291 to 293
except json.JSONDecodeError:
# If JSON parsing fails, return original output
pass
Copy link
Member

Choose a reason for hiding this comment

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

Return an error indicating the parse failure:

Suggested change
except json.JSONDecodeError:
# If JSON parsing fails, return original output
pass
except json.JSONDecodeError as e:
result["output"] = f"Failed to parse experiment data: {e}"
result["error"] = True

Comment on lines 282 to 287
output_lines = ["NAME\t\tSTATUS\t\tAGE"]
for exp in filtered:
name = exp.get("metadata", {}).get("name", "")
status = exp.get("status", {}).get("phase", "Unknown")
created = exp.get("metadata", {}).get("creationTimestamp", "")
output_lines.append(f"{name}\t{status}\t{created}")
Copy link
Member

Choose a reason for hiding this comment

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

The header uses double tabs (\t\t) but the data rows use single tabs (\t). This causes misaligned columns. Use consistent tab counts or, better, use string formatting with fixed widths.

- Add _format_age helper to convert ISO timestamps to relative age (5m, 2h, 3d)
- Use direct TypedDict access (result[key]) instead of .get() for consistency
- Improve error handling: return error message on JSONDecodeError
- Fix column alignment using f-string width formatting
- Remove redundant comments
@MasterAffan
Copy link
Author

@KaranJagtiani i have made the changes please have a look

rollout_name: Optional[str] = Field(
default=None,
description="The name of the rollout to get experiments for (if not specified, gets all experiments)",
description="The name of the rollout to filter experiments by. Experiments are filtered by checking if their name starts with the rollout name (Argo Rollouts naming convention: {rollout-name}-{hash}-{revision}-{step}). If not specified, returns all experiments.",
Copy link
Member

Choose a reason for hiding this comment

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

Why was this changed?

Copy link
Member

@KaranJagtiani KaranJagtiani left a comment

Choose a reason for hiding this comment

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

  1. When all_namespaces is true you still return a table without namespace. That drops required context and is a regression vs kubectl get -A. Include NAMESPACE in the header and rows when all_namespaces is set, or always include it.
  2. Add test coverage

Comment on lines +270 to +275
"""Get Argo Rollouts experiments, optionally filtered by rollout name.
When rollout_name is provided, experiments are filtered by checking if their
name starts with the rollout name, following Argo Rollouts' naming convention
where experiment names are: {rollout-name}-{podHash}-{revision}-{stepIndex}.
"""
Copy link
Member

Choose a reason for hiding this comment

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

Revert.

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.

Fix argo_list_experiments ignoring rollout_name parameter

2 participants