Skip to content

Conversation

alienzach
Copy link
Contributor

@alienzach alienzach commented Sep 18, 2025

Add support for ignore files in deploy bundling

TL;DR

Added support for gitignore-style pattern matching during deployment to exclude files from being bundled.

What changed?

  • Added two new CLI options to the deploy command:
    • --ignore: Uses .mcpacignore in the current directory
    • --ignore-file <path>: Specifies a custom ignore file path
  • Created a new bundle_utils.py module with utilities for:
    • Parsing gitignore-style patterns into a PathSpec matcher
    • Filtering files during the bundling process
  • Enhanced the bundling process to respect ignore patterns while preserving default excludes
  • Updated documentation with examples and usage information

How to use?

# Use default .mcpacignore in current directory
mcp-agent deploy my-agent --ignore

# Use a custom ignore file
mcp-agent deploy my-agent --ignore-file .deployignore

# Both options can be combined (--ignore-file takes precedence)
mcp-agent deploy my-agent --ignore --ignore-file .customignore

Patterns follow gitignore syntax and are evaluated relative to the project directory.

Why make this change?

This change gives users more control over which files are included in their deployment bundles. It allows:

  • Excluding large files or directories that aren't needed for deployment
  • Preventing sensitive files from being bundled
  • Following standard gitignore patterns that developers are already familiar with
  • Customizing ignore rules per project or deployment

Summary by CodeRabbit

  • New Features
    • Deploy supports gitignore-style exclusions via an ignore file; preserves default excludes when none provided.
    • Added --ignore-file option with auto-detect of .mcpacignore; explicit --ignore-file takes precedence.
    • Deployment now logs bundled project files (excluding internal helpers) before packaging.
  • Documentation
    • CLI reference and cloud overview updated with ignore-file behavior, precedence, and examples.
  • Tests
    • Expanded tests for ignore-file detection, precedence, pathspec-driven ignores, and secret handling.
  • Chores
    • Added optional dependency pathspec>=0.12.1 for ignore pattern support.

Copy link

coderabbitai bot commented Sep 18, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds optional dependency pathspec; introduces gitignore-aware bundle utilities and threads a new --ignore-file option through deploy CLI into the bundling flow, applying gitignore-based ignores during copy; updates docs and tests to validate behavior and precedence.

Changes

Cohort / File(s) Summary
Dependencies
pyproject.toml
Adds optional CLI dependency: pathspec>=0.12.1.
Bundle utilities (new)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py
New module: create_pathspec_from_gitignore(ignore_file_path: Path) builds a pathspec.PathSpec from a gitignore-style file; should_ignore_by_gitignore(path_str, names, project_dir, spec) provides a shutil.copytree-compatible ignore adapter returning names to skip.
Wrangler wrapper updates
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py
`wrangler_deploy(..., ignore_file: Path
Deploy CLI flow
src/mcp_agent/cli/cloud/commands/deploy/main.py
Adds CLI option --ignore-file (Optional[Path]); resolves effective ignore path (explicit or .mcpacignore in config/CWD) and passes ignore into _deploy_with_retry and wrangler_deploy.
Tests
tests/cli/commands/test_wrangler_wrapper.py, tests/cli/commands/test_deploy_command.py
Adds unit and integration tests for pathspec utilities and deploy ignore-file behavior: auto-detection, explicit --ignore-file precedence, missing-ignore warnings, directory/nested pattern handling, and bundled-file expectations (including secrets/example handling).
Docs
docs/cli-reference.mdx, docs/cloud/overview.mdx
Documents --ignore-file option, .mcpacignore detection order and precedence, gitignore-syntax behavior, and usage examples.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as Deploy CLI
  participant Main as deploy_config/_deploy_with_retry
  participant WW as wrangler_wrapper
  participant BU as bundle_utils
  participant FS as Filesystem
  participant Cloud as Deployer

  User->>CLI: run deploy [--ignore | --ignore-file]
  CLI->>Main: deploy_config(..., ignore_path)
  Main->>WW: wrangler_deploy(..., ignore_file=ignore_path)

  WW->>FS: read project directory
  WW->>BU: create_pathspec_from_gitignore(ignore_file)
  BU-->>WW: PathSpec | None

  rect rgba(200,230,255,0.25)
    WW->>FS: copytree(project -> temp, ignore=adapter)
    WW->>BU: should_ignore_by_gitignore(path, names, project_dir, spec)
    BU-->>WW: names to skip
    WW->>FS: skip or copy entries accordingly
  end

  WW->>FS: compute workspace fingerprint (apply same ignore set)
  WW->>Cloud: upload bundle + metadata
  Cloud-->>WW: result
  WW-->>Main: return status
  Main-->>CLI: report result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • saqadri
  • petersonbill64

Poem

I nibble through ignores with tiny feet,
Skipping crumbs no bundle should keep,
Secrets tucked safe, examples kept bright,
I pack what's needed and hop into the night,
A rabbit's bundle—ready for flight. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “feat: bundle comments and file ignore” is misleading because while the pull request adds gitignore-style file ignore support and new bundling utilities, it does not introduce any feature related to “bundling comments,” so the title only partially reflects the actual changes and includes an incorrect aspect. Please update the title to clearly summarize the main change, for example “feat: add gitignore-style ignore support to deployment bundler” or similar wording that focuses on the new file-ignore functionality and bundle utilities.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 09-18-feat_bundle_comments_and_file_ignore

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
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@alienzach alienzach force-pushed the 09-18-feat_bundle_comments_and_file_ignore branch from 0bfa67b to a7d6259 Compare September 22, 2025 18:44
@alienzach alienzach marked this pull request as ready for review September 22, 2025 19:59
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enhances deployment security by adding support for .gitignore patterns and comment cleaning to prevent sensitive information from being included in deployed bundles. The implementation respects gitignore rules during file copying and automatically removes comments from Python and YAML files that might contain secrets.

  • Added gitignore pattern support using the pathspec library for consistent file exclusion
  • Implemented comment and docstring cleaning for Python and YAML files to prevent secret leakage
  • Enhanced wrangler_deploy to apply both gitignore filtering and comment cleaning with user feedback

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

File Description
bundle_utils.py New module with gitignore parsing, comment cleaning utilities for Python/YAML files
wrangler_wrapper.py Enhanced deploy function to use gitignore patterns and clean comments before bundling
test_wrangler_wrapper.py Comprehensive tests for new bundle utilities and deploy enhancements
pyproject.toml Added pathspec dependency for gitignore pattern matching

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyproject.toml (1)

79-83: Make pathspec a required dependency or lazy-import it to avoid runtime ImportError

  • Problem: src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py performs a top-level import pathspec and src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py imports bundle_utils at module import time; pyproject.toml lists pathspec only under [project.optional-dependencies].cli, so importing these modules without installing [cli] extras will raise ImportError at runtime.
  • Fix (pick one): move "pathspec>=0.12.1" from [project.optional-dependencies].cli into [project].dependencies in pyproject.toml; OR change bundle_utils.py to lazy-import pathspec inside create_pathspec_from_gitignore (wrap import in try/except ImportError, return None or raise a clear error when gitignore support is requested).
  • Optional: if keeping it optional, update README to recommend pip install "mcp-agent[cli]".
🧹 Nitpick comments (3)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)

28-65: Unused helper and weak return typing

create_gitignore_matcher isn’t used. Either use it in wrangler_wrapper or remove it. Also, return type should be Callable[[Path, str], bool] instead of callable.

Apply this diff for typing:

-from typing import Optional, Set
+from typing import Optional, Set, Callable
@@
-def create_gitignore_matcher(project_dir: Path) -> callable:
+def create_gitignore_matcher(project_dir: Path) -> Callable[[Path, str], bool]:

67-105: Normalize paths for cross‑platform gitignore matching

Use POSIX paths; PathSpec patterns are POSIX‑style. This avoids Windows separator mismatches.

Apply this diff:

-def should_ignore_by_gitignore(path_str: str, names: list, project_dir: Path, spec: Optional[pathspec.PathSpec]) -> Set[str]:
+def should_ignore_by_gitignore(path_str: str, names: list, project_dir: Path, spec: Optional[PathSpecT]) -> Set[str]:
@@
-            rel_path_str = str(rel_path)
+            rel_path_str = rel_path.as_posix()
tests/cli/commands/test_wrangler_wrapper.py (1)

1060-1093: Avoid monkey‑patching Path.is_dir globally

Overwriting Path.is_dir can leak across tests under parallel runs. Use monkeypatch fixture to patch Path.is_dir within scope.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d954ea6 and ca4da7b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • pyproject.toml (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (3 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (4)
  • create_pathspec_from_gitignore (10-25)
  • should_ignore_by_gitignore (67-104)
  • clean_yaml_files_in_directory (253-297)
  • clean_python_files_in_directory (300-328)
src/mcp_agent/cli/utils/ux.py (1)
  • print_info (32-49)
tests/cli/commands/test_wrangler_wrapper.py (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (6)
  • create_pathspec_from_gitignore (10-25)
  • should_ignore_by_gitignore (67-104)
  • clean_yaml_comments (107-126)
  • clean_python_comments (129-250)
  • clean_yaml_files_in_directory (253-297)
  • clean_python_files_in_directory (300-328)
🔇 Additional comments (9)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)

165-168: Good: parse .gitignore once and reuse

Creating the PathSpec once avoids repeated parsing during copy. Looks solid.


254-265: Nice touch: include secrets example in workspace hash ignore

Avoids non-functional diffs in fingerprints. LGTM.

src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1)

300-328: Directory cleaners: good error tolerance

Skips unreadable files and only writes on change. Nice.

tests/cli/commands/test_wrangler_wrapper.py (6)

7-7: Good: bring in mock_open for pathspec tests

Imports look appropriate.


928-1023: Great coverage for secrets and example exclusion

Asserts both real and example secrets are excluded from temp and original remain intact. LGTM.


1033-1056: Nice: verifies PathSpec parsing and patterns

Solid unit test for .gitignore parsing behavior.


1095-1156: Python cleaner test hits the right cases

Validates docstring and inline comment removal while preserving string #. Once cleaner is fixed (no extra pass), expectations remain valid.

Please re-run this test after applying the clean_python_comments fix to ensure no regressions.


1247-1297: Good end‑to‑end for .gitignore honoring

Confirms ignored artifacts aren’t copied and included files are processed. Nice.


1300-1356: Useful assertions on cleaning + UX messaging

Catches both actual content sanitization and the info logs. 👍

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

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

I think the gitignore handling looks great. I feel pretty strongly that we shouldn't be stripping comments out of py files, and probably not from yaml files, either

Copy link
Collaborator

@saqadri saqadri left a comment

Choose a reason for hiding this comment

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

+1 to @rholinshead's review comments.
In addition, I want to understand what the motivation for this change is. Why exclude things based on gitignore patterns? What if they are needed for the agent to run/function? For example, what if I have gitignored the deployed.secrets.yaml file?

@alienzach alienzach force-pushed the 09-18-feat_bundle_comments_and_file_ignore branch from ca4da7b to 6c94486 Compare September 25, 2025 01:31
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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca4da7b and 6c94486.

📒 Files selected for processing (4)
  • pyproject.toml (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (3 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pyproject.toml
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py
🧰 Additional context used
🧬 Code graph analysis (1)
tests/cli/commands/test_wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (6)
  • create_pathspec_from_gitignore (10-25)
  • should_ignore_by_gitignore (67-104)
  • clean_yaml_comments (107-126)
  • clean_python_comments (129-250)
  • clean_yaml_files_in_directory (253-297)
  • clean_python_files_in_directory (300-328)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)
  • wrangler_deploy (116-358)
🔇 Additional comments (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1)

190-251: Stop truncating multi-line Python literals when stripping comments

After ast.unparse, the cleaned_code string already has comments removed. The extra manual pass starting on Line 193 resets in_string for every physical line. When the unparser emits a multi-line literal (e.g., return """line1\n# keep\nline3"""), the middle line is processed with in_string = False, so the leading # is treated as a comment and the entire line is dropped. This corrupts legitimate string content before deployment.

Apply this diff so we rely on ast.unparse (which preserves literals) and fall back to the original source if the parse fails:

-        # Compile back to code
-        cleaned_code = ast.unparse(tree)
-
-        # Now remove inline comments (lines starting with #)
-        lines = []
-        for line in cleaned_code.split('\n'):
-            # Remove lines that are only comments
-            stripped = line.lstrip()
-            if stripped and not stripped.startswith('#'):
-                # Remove inline comments from the line
-                # Find # not inside strings
-                in_string = False
-                string_char = None
-                cleaned_line = []
-                i = 0
-                while i < len(line):
-                    char = line[i]
-
-                    # Handle string boundaries
-                    if char in ('"', "'") and (i == 0 or line[i-1] != '\\'):
-                        if not in_string:
-                            in_string = True
-                            string_char = char
-                        elif char == string_char:
-                            in_string = False
-
-                    # If we find # outside a string, truncate here
-                    if char == '#' and not in_string:
-                        # Remove trailing whitespace
-                        result = ''.join(cleaned_line).rstrip()
-                        if result:  # Only add non-empty lines
-                            lines.append(result)
-                        break
-
-                    cleaned_line.append(char)
-                    i += 1
-                else:
-                    # No comment found, add the whole line if non-empty
-                    if line.strip():
-                        lines.append(line.rstrip())
-
-        return '\n'.join(lines)
+        return ast.unparse(tree)
@@
-    except SyntaxError:
-        # If parsing fails, at least try to remove simple comments
-        lines = []
-        for line in python_content.split('\n'):
-            stripped = line.lstrip()
-            if stripped and not stripped.startswith('#'):
-                # Try to remove inline comments (basic approach)
-                if '#' in line:
-                    # Find first # not in a string (simple heuristic)
-                    parts = line.split('#')
-                    if parts[0].strip():
-                        lines.append(parts[0].rstrip())
-                else:
-                    lines.append(line.rstrip())
-            elif not stripped.startswith('#'):
-                # Keep empty lines for structure
-                lines.append('')
-        return '\n'.join(lines)
+    except SyntaxError:
+        # On parse failure, keep the original source to avoid corrupting code
+        return python_content

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

299-307: Pass effective ignore path derived from flags

Integrate new flags: explicit --ignore-file takes precedence; otherwise use .mcpacignore when --ignore is set.

Apply this diff:

         app = run_async(
             _deploy_with_retry(
                 app_id=app_id,
                 api_key=effective_api_key,
                 project_dir=config_dir,
                 mcp_app_client=mcp_app_client,
                 retry_count=retry_count,
-                ignore=ignore,
+                ignore_file=(
+                    ignore_file
+                    or (config_dir / ".mcpacignore" if use_ignore else None)
+                ),
             )
         )
🧹 Nitpick comments (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1)

61-62: Minor typing nit: prefer Sequence[str] for names

More accurate and flexible than list for the shutil.copytree callback.

Apply this diff:

-def should_ignore_by_gitignore(
-    path_str: str, names: list, project_dir: Path, spec: Optional[PathSpecT]
+from typing import Sequence
+
+def should_ignore_by_gitignore(
+    path_str: str, names: Sequence[str], project_dir: Path, spec: Optional[PathSpecT]
 ) -> Set[str]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c94486 and ce313b3.

📒 Files selected for processing (6)
  • docs/cli-reference.mdx (3 hunks)
  • docs/cloud/overview.mdx (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (5 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
✅ Files skipped from review due to trivial changes (2)
  • docs/cloud/overview.mdx
  • docs/cli-reference.mdx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py
  • tests/cli/commands/test_wrangler_wrapper.py
🔇 Additional comments (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1)

14-14: Make pathspec optional: lazy import + safe type hints to avoid hard dependency

Top‑level import forces the optional dependency at import time and annotations reference it eagerly. Degrade gracefully when pathspec isn’t installed.

Apply this diff:

@@
-from pathlib import Path
-from typing import Optional, Set
-import pathspec
+from pathlib import Path
+from typing import Optional, Set, TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    import pathspec as _pathspec
+    PathSpecT = _pathspec.PathSpec
+else:
+    PathSpecT = Any
@@
-def create_pathspec_from_gitignore(ignore_file_path: Path) -> Optional[pathspec.PathSpec]:
+def create_pathspec_from_gitignore(ignore_file_path: Path) -> Optional[PathSpecT]:
@@
-    with open(ignore_file_path, "r", encoding="utf-8") as f:
-        spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
+    try:
+        import pathspec as _pathspec  # type: ignore
+    except Exception:
+        return None
+    with open(ignore_file_path, "r", encoding="utf-8") as f:
+        spec = _pathspec.PathSpec.from_lines("gitwildmatch", f)
@@
-def should_ignore_by_gitignore(
-    path_str: str, names: list, project_dir: Path, spec: Optional[pathspec.PathSpec]
-) -> Set[str]:
+def should_ignore_by_gitignore(
+    path_str: str, names: list, project_dir: Path, spec: Optional[PathSpecT]
+) -> Set[str]:

Also applies to: 17-18, 33-36, 41-43

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: 0

🧹 Nitpick comments (5)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)

114-116: Docstring: add missing ignore_file parameter

The Args section doesn’t document ignore_file. Recommend updating to clarify accepted files (e.g., .mcpacignore or .gitignore) and precedence.

Outside-diff snippet for the docstring:

    Args:
        app_id (str): The application ID.
        api_key (str): User MCP Agent Cloud API key.
        project_dir (Path): The directory of the project to deploy.
        ignore_file (Path | None): Optional path to an ignore file (gitignore syntax).
            If provided, its patterns are applied in addition to default excludes.

165-178: Fallback to project .gitignore when no explicit ignore_file is provided

Today we only apply ignore patterns if ignore_file is passed. To “respect .gitignore patterns,” fall back to project_dir/.gitignore when ignore_file is None. Keep current logging and defaults if no file exists.

Apply:

-        # Load ignore rules (gitignore syntax) only if an explicit ignore file is provided
-        ignore_spec = (
-            create_pathspec_from_gitignore(ignore_file) if ignore_file else None
-        )
-        if ignore_file:
-            if ignore_spec is None:
-                print_warning(
-                    f"Ignore file '{ignore_file}' not found; applying default excludes only"
-                )
-            else:
-                print_info(f"Using ignore patterns from {ignore_file}")
-        else:
-            print_info("No ignore file provided; applying default excludes only")
+        # Load ignore rules (gitignore syntax). If no explicit file, try project .gitignore
+        if ignore_file:
+            ignore_spec = create_pathspec_from_gitignore(ignore_file)
+            if ignore_spec is None:
+                print_warning(
+                    f"Ignore file '{ignore_file}' not found; applying default excludes only"
+                )
+            else:
+                print_info(f"Using ignore patterns from {ignore_file}")
+        else:
+            default_ignore = project_dir / ".gitignore"
+            ignore_spec = create_pathspec_from_gitignore(default_ignore)
+            if ignore_spec:
+                print_info(f"Using ignore patterns from {default_ignore}")
+            else:
+                print_info("No ignore file provided; applying default excludes only")

Additionally, consider using pathspec.GitIgnoreSpec in create_pathspec_from_gitignore to get full gitignore semantics (including negations). This can simplify trailing-slash handling in should_ignore_by_gitignore.

tests/cli/commands/test_wrangler_wrapper.py (3)

924-1020: Add coverage to exclude .env by default

Once .env is excluded, add a focused test to assert it’s not copied to temp.

Proposed test:

def test_wrangler_deploy_env_file_exclusion():
    with tempfile.TemporaryDirectory() as temp_dir:
        project_path = Path(temp_dir)
        (project_path / "main.py").write_text("from mcp_agent_cloud import MCPApp\napp = MCPApp(name='test')")
        (project_path / ".env").write_text("API_KEY=secret\n")

        def check_env_excluded(*args, **kwargs):
            temp_project_dir = Path(kwargs["cwd"])
            assert not (temp_project_dir / ".env").exists()
            return MagicMock(returncode=0)

        with patch("subprocess.run", side_effect=check_env_excluded):
            wrangler_deploy("test-app", "test-api-key", project_path)

1025-1049: Consider testing gitignore negations and anchored dir patterns

If create_pathspec_from_gitignore switches to GitIgnoreSpec, add a negation case (e.g., *.log then !keep.log) and a dir pattern case (dir/). This guards semantics users expect from .gitignore.


1052-1085: Use pytest.monkeypatch instead of overriding Path.is_dir globally

Directly replacing Path.is_dir is risky. Prefer monkeypatch for safer, scoped patching.

Example:

def test_should_ignore_by_gitignore(monkeypatch):
    import pathspec
    gitignore_content = "*.log\n*.pyc\nnode_modules/\n"
    spec = pathspec.PathSpec.from_lines('gitwildmatch', gitignore_content.splitlines())
    project_dir = Path("/fake/project")
    names = ["test.log", "main.py", "node_modules", "config.yaml", "test.pyc"]

    def fake_is_dir(p: Path) -> bool:
        return p.name in {"node_modules"}

    monkeypatch.setattr(Path, "is_dir", fake_is_dir, raising=True)
    ignored = should_ignore_by_gitignore(str(project_dir), names, project_dir, spec)
    assert {"test.log", "test.pyc", "node_modules"} <= ignored
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce313b3 and 4888557.

📒 Files selected for processing (2)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (4 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/cli/commands/test_wrangler_wrapper.py (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-77)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-77)
src/mcp_agent/cli/utils/ux.py (2)
  • print_warning (66-77)
  • print_info (32-49)
🔇 Additional comments (8)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (5)

21-24: LGTM: imports for ignore utilities

Imports are scoped and minimal. Good separation via bundle_utils.


194-199: Confirm precedence: hardcoded excludes shadow ignore_file re-includes

Because default excludes are applied first, ignore_file negations (e.g., !.somefile) cannot re-include those entries. If you want ignore_file to be authoritative, invert precedence or support a small allowlist override.


233-260: LGTM: clear, stable logging of bundled files

Nice touch mapping .mcpac.py back to original names and sorting for stable output.


183-191: Exclude .env to avoid secret leakage (remove whitelist)

The current logic explicitly whitelists .env, risking secret exposure. Exclude all dotfiles (including .env) by default.

Apply:

-            for name in names:
-                if (name.startswith(".") and name not in {".env"}) or name in {
+            for name in names:
+                if name.startswith(".") or name in {
                     "logs",
                     "__pycache__",
                     "node_modules",
                     "venv",
                     MCP_SECRETS_FILENAME,  # Exclude mcp_agent.secrets.yaml only
                 }:
                     ignored.add(name)

114-116: Call sites updated for new ignore_file parameter
Verified all internal callers (CLI wiring and tests) either pass ignore_file explicitly or rely on its default None. No further changes required.

tests/cli/commands/test_wrangler_wrapper.py (3)

20-23: LGTM: importing bundle utils for targeted tests

Good modularization of tests around the new ignore utilities.


924-1020: LGTM: secrets file exclusion test

Covers the live secrets file correctly while allowing the example file.


1087-1137: LGTM: ignore_file respected end-to-end

Good scenario coverage verifying ignores aren’t copied and non-ignored files are processed.

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 (2)
docs/cli-reference.mdx (1)

277-283: Clarify default ignore-file location

Docs say patterns are evaluated relative to --config-dir, but --ignore currently picks .mcpacignore from the current working directory. That’s confusing when --config-dir points elsewhere. Please update the docs (or the implementation) so the default file location and evaluation root align.

tests/cli/commands/test_deploy_command.py (1)

148-205: Default ignore path should follow config directory

You only assert the bare --ignore forwards Path(".mcpacignore"). This passes because the test chdirs into the config dir, but that hides a behavioural mismatch: the CLI always uses the caller’s CWD, while wrangler_deploy later resolves the ignore file against project_dir. Add a test where --config-dir differs from CWD to document—and ideally rework—the intended behaviour.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4888557 and 4a57ba3.

📒 Files selected for processing (5)
  • docs/cli-reference.mdx (3 hunks)
  • docs/cloud/overview.mdx (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • tests/cli/commands/test_deploy_command.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py
  • docs/cloud/overview.mdx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-22T18:59:49.368Z
Learnt from: CR
PR: lastmile-ai/mcp-agent#0
File: examples/usecases/reliable_conversation/CLAUDE.md:0-0
Timestamp: 2025-07-22T18:59:49.368Z
Learning: Applies to examples/usecases/reliable_conversation/examples/reliable_conversation/src/**/*.py : Use mcp-agent's Agent abstraction for ALL LLM interactions, including quality evaluation, to ensure consistent tool access, logging, and error handling.

Applied to files:

  • docs/cli-reference.mdx
🧬 Code graph analysis (2)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
tests/cli/commands/test_deploy_command.py (2)
src/mcp_agent/cli/mcp_app/api_client.py (2)
  • get_app_id_by_name (286-308)
  • create_app (134-164)
src/mcp_agent/cli/mcp_app/mock_client.py (2)
  • get_app_id_by_name (36-45)
  • create_app (86-124)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: update_release_draft
  • GitHub Check: update_release_draft
🔇 Additional comments (1)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

339-365: Propagate resolved ignore path

After updating the default above, keep _deploy_with_retry receiving an already-resolved ignore path; no code change needed here once ignore_path is fixed.

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: 0

🧹 Nitpick comments (3)
tests/cli/commands/test_deploy_command.py (1)

121-137: Pass config_dir as string to Typer for consistency

Click/Typer expects CLI args as strings. Other tests use str(temp_config_dir); mirror that here to avoid incidental type issues.

Apply this diff:

-                    temp_config_dir,
+                    str(temp_config_dir),
tests/cli/commands/test_wrangler_wrapper.py (1)

1052-1085: Avoid monkeypatching pathlib.Path globally; use pytest monkeypatch

Directly assigning Path.is_dir risks leakage if restoration fails. Prefer the monkeypatch fixture for safer, scoped patching.

Apply this diff to use monkeypatch:

-def test_should_ignore_by_gitignore():
+def test_should_ignore_by_gitignore(monkeypatch):
@@
-    original_is_dir = Path.is_dir
-    Path.is_dir = lambda self: self.name in ["node_modules", "temp", "build"]
-
-    try:
-        ignored = should_ignore_by_gitignore(current_path, names, project_dir, spec)
-    finally:
-        # Restore original method
-        Path.is_dir = original_is_dir
+    monkeypatch.setattr(
+        Path,
+        "is_dir",
+        lambda self: self.name in ["node_modules", "temp", "build"],
+        raising=True,
+    )
+    ignored = should_ignore_by_gitignore(current_path, names, project_dir, spec)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

346-367: Rename _deploy_with_retry param to ignore_file for clarity

Aligns with wrangler_deploy’s keyword, avoids confusion between boolean flag vs path.

Apply this diff:

-async def _deploy_with_retry(
+async def _deploy_with_retry(
@@
-    ignore: Optional[Path],
+    ignore_file: Optional[Path],
 ):
@@
-        wrangler_deploy(
+        wrangler_deploy(
             app_id=app_id,
             api_key=api_key,
             project_dir=project_dir,
-            ignore_file=ignore,
+            ignore_file=ignore_file,
         )

And update the call site above:

-                ignore=ignore_path,
+                ignore_file=ignore_path,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a57ba3 and d2842e6.

📒 Files selected for processing (3)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • tests/cli/commands/test_deploy_command.py (2 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/cli/commands/test_wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)
  • wrangler_deploy (114-387)
tests/cli/commands/test_deploy_command.py (2)
src/mcp_agent/cli/mcp_app/api_client.py (2)
  • get_app_id_by_name (286-308)
  • create_app (134-164)
src/mcp_agent/cli/mcp_app/mock_client.py (2)
  • get_app_id_by_name (36-45)
  • create_app (86-124)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
🔇 Additional comments (12)
tests/cli/commands/test_deploy_command.py (5)

77-79: Help text assertions for new flags look good

Validates presence of --ignore and --ignore-file in help output.


138-146: Basic deploy flow assertions are fine

Exit code and key log messages are asserted; wrangler_deploy is isolated via patch.


148-203: Default ignore file selection behavior is correctly validated

Bare --ignore uses config_dir/.mcpacignore and passes its absolute path through.


205-262: Custom --ignore-file propagation test is solid

Verifies resolved absolute path is forwarded to bundler.


264-323: Precedence test is correct

Explicit --ignore-file overrides the default from --ignore.

tests/cli/commands/test_wrangler_wrapper.py (5)

20-23: Good coverage for ignore utilities

Importing and testing create_pathspec_from_gitignore and should_ignore_by_gitignore improves confidence in bundling behavior.


925-951: Secrets example file handling covered

Ensures live secrets are excluded and the example file is included and renamed with .mcpac.py.


972-1014: Strong assertions for secrets exclusion

Clear checks that the live secrets file is not copied or transformed while example and other YAML files are processed.


1025-1040: PathSpec creation test is appropriate

Mocks file existence and validates typical patterns.


1087-1138: Ignore-file integration test looks correct

Affirms Wrangler respects gitignore-like patterns and only copies/renames allowed files.

src/mcp_agent/cli/cloud/commands/deploy/main.py (2)

300-309: Effective ignore path resolution is correct

Precedence of explicit --ignore-file over default .mcpacignore in config_dir is handled cleanly; only passes default if it exists.


107-121: Help text says CWD, but behavior uses config_dir

The default ignore file is resolved against config_dir (correct). Update help to match and consider validating existence for --ignore-file.

Apply this diff:

-    ignore: bool = typer.Option(
-        False,
-        "--ignore",
-        help="Use .mcpacignore in CWD to ignore files",
-    ),
+    ignore: bool = typer.Option(
+        False,
+        "--ignore",
+        help="Use .mcpacignore in the config directory to ignore files",
+    ),
@@
-    ignore_file: Optional[Path] = typer.Option(
+    ignore_file: Optional[Path] = typer.Option(
         None,
         "--ignore-file",
-        help="Specify custom ignore file (gitignore syntax)",
-        exists=False,
+        help="Specify custom ignore file (gitignore syntax).",
+        exists=True,
         readable=True,
         dir_okay=False,
         file_okay=True,
         resolve_path=True,
     ),

Copy link
Contributor Author

@alienzach alienzach left a comment

Choose a reason for hiding this comment

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

Tested locally with examples and working as expected.

Screenshot 2025-09-25 at 1.22.20 PM.png

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

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

Looks good in general but a couple things:

  • from UX perspective, seems weird to have two different ignore options; I would recommend just the one for overriding ignore file path and otherwise use .mcpacignore if it exists in the project dir
  • I don't think we want to tell the user about any 'internal' files included in the deployment since they are implementation details and will just be confusing

Also a comment on the logs dir handling, which may make more sense in a followup

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: 0

🧹 Nitpick comments (7)
tests/cli/commands/test_deploy_command.py (7)

121-135: Pass strings to runner.invoke for consistency.

Click/Typer expects string args. Other tests cast to str(temp_config_dir); do the same here to avoid surprises.

Apply this diff:

             result = runner.invoke(
                 app,
                 [
                     "deploy",
                     MOCK_APP_NAME,
                     "--config-dir",
-                    temp_config_dir,
+                    str(temp_config_dir),
                     "--api-url",
                     "http://test-api.com",
                     "--api-key",
                     "test-api-key",
                     "--non-interactive",  # Prevent prompting for input
                 ],
             )

160-163: Make the wrangler capture resilient to signature changes and normalize paths.

Capture by position is brittle if the function signature changes; also normalize to resolved Path to avoid type mismatches in assertions.

Apply this diff:

-    def _capture_wrangler(app_id, api_key, project_dir, ignore_file=None):
-        captured["ignore_file"] = ignore_file
-        return MOCK_APP_ID
+    def _capture_wrangler(*args, **kwargs):
+        ignore_file = kwargs.get("ignore_file") or (args[3] if len(args) > 3 else None)
+        captured["ignore_file"] = Path(ignore_file).resolve() if ignore_file else None
+        return MOCK_APP_ID

217-220: Repeat resilient capture pattern here.

Mirror the robust *args, **kwargs capture and path normalization to avoid brittle positional assumptions.

Apply this diff:

-    def _capture_wrangler(app_id, api_key, project_dir, ignore_file=None):
-        captured["ignore_file"] = ignore_file
-        return MOCK_APP_ID
+    def _capture_wrangler(*args, **kwargs):
+        ignore_file = kwargs.get("ignore_file") or (args[3] if len(args) > 3 else None)
+        captured["ignore_file"] = Path(ignore_file).resolve() if ignore_file else None
+        return MOCK_APP_ID

273-276: Repeat resilient capture pattern here as well.

Same rationale: future-proof and normalize paths.

Apply this diff:

-    def _capture_wrangler(app_id, api_key, project_dir, ignore_file=None):
-        captured["ignore_file"] = ignore_file
-        return MOCK_APP_ID
+    def _capture_wrangler(*args, **kwargs):
+        ignore_file = kwargs.get("ignore_file") or (args[3] if len(args) > 3 else None)
+        captured["ignore_file"] = Path(ignore_file).resolve() if ignore_file else None
+        return MOCK_APP_ID

315-316: Make the assertion robust to string vs Path.

If upstream ever passes a string, this still works after normalizing in the capture. Alternatively, coerce at assertion time.

Optionally coerce in the assertion:

-    assert captured.get("ignore_file") == custom_ignore.resolve()
+    assert Path(captured.get("ignore_file")).resolve() == custom_ignore.resolve()

334-337: Repeat resilient capture pattern here too.

Keep capture consistent across tests.

Apply this diff:

-    def _capture_wrangler(app_id, api_key, project_dir, ignore_file=None):
-        captured["ignore_file"] = ignore_file
-        return MOCK_APP_ID
+    def _capture_wrangler(*args, **kwargs):
+        ignore_file = kwargs.get("ignore_file") or (args[3] if len(args) > 3 else None)
+        captured["ignore_file"] = Path(ignore_file).resolve() if ignore_file else None
+        return MOCK_APP_ID

319-377: Add tests for edge cases: invalid path and both flags set.

  • Invalid --ignore-file path should produce a clear error and non‑zero exit.
  • When both --ignore and --ignore-file are provided, verify precedence (custom path wins).

I can draft these tests mirroring your existing patterns. Want me to add them?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2842e6 and 51daafb.

📒 Files selected for processing (4)
  • docs/cli-reference.mdx (3 hunks)
  • docs/cloud/overview.mdx (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • tests/cli/commands/test_deploy_command.py (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/cli-reference.mdx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py
  • docs/cloud/overview.mdx
🧰 Additional context used
🧬 Code graph analysis (1)
tests/cli/commands/test_deploy_command.py (2)
src/mcp_agent/cli/mcp_app/api_client.py (2)
  • get_app_id_by_name (286-308)
  • create_app (134-164)
src/mcp_agent/cli/mcp_app/mock_client.py (2)
  • get_app_id_by_name (36-45)
  • create_app (86-124)
🔇 Additional comments (2)
tests/cli/commands/test_deploy_command.py (2)

77-79: Consider asserting presence of --ignore as well (if supported).

Help checks cover --ignore-file and .mcpacignore. If the CLI also exposes --ignore (per PR summary), add an assertion for it to prevent regressions. If auto-detection replaced the flag, ignore this.

Based on PR objectives


199-201: Assertion stays valid after path normalization.

With the proposed capture change, captured["ignore_file"] is a resolved Path; the equality with default_ignore.resolve() remains correct.

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

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

Just one final comment on the printing of the bundled files, to avoid a string of "Info: " -prefixed logs

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)

114-137: Update docstring to document ignore_file parameter.

The function signature now includes ignore_file: Path | None = None, but the docstring doesn't describe this parameter.

Apply this diff:

 def wrangler_deploy(
     app_id: str, api_key: str, project_dir: Path, ignore_file: Path | None = None
 ) -> None:
     """Bundle the MCP Agent using Wrangler.
 
     A thin wrapper around the Wrangler CLI to bundle the MCP Agent application code
     and upload it our internal cf storage.
 
     Some key details here:
     - We copy the user's project to a temporary directory and perform all operations there
     - Secrets file must be excluded from the bundle
     - We must add a temporary `wrangler.toml` to the project directory to set python_workers
       compatibility flag (CLI arg is not sufficient).
     - Python workers with a `requirements.txt` file cannot be published by Wrangler, so we must
       rename any `requirements.txt` file to `requirements.txt.mcpac.py` before bundling
     - Non-python files (e.g. `uv.lock`, `poetry.lock`, `pyproject.toml`) would be excluded by default
     due to no py extension, so they are renamed with a `.mcpac.py` extension.
     - We exclude .venv directories from the copy to avoid bundling issues.
 
     Args:
         app_id (str): The application ID.
         api_key (str): User MCP Agent Cloud API key.
         project_dir (Path): The directory of the project to deploy.
+        ignore_file (Path | None): Optional path to a gitignore-style file for excluding files from the bundle.
     """
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93eadef and 660ac95.

📒 Files selected for processing (4)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (4 hunks)
  • tests/cli/commands/test_deploy_command.py (3 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py
🧰 Additional context used
🧬 Code graph analysis (3)
tests/cli/commands/test_wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)
  • wrangler_deploy (114-378)
tests/cli/commands/test_deploy_command.py (1)
src/mcp_agent/cli/mcp_app/mock_client.py (2)
  • get_app_id_by_name (36-45)
  • create_app (86-124)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/utils/ux.py (2)
  • print_warning (66-77)
  • print_info (32-49)
🪛 GitHub Actions: Pull Request Checks
tests/cli/commands/test_wrangler_wrapper.py

[error] 7-7: Ruff check: unused import 'unittest.mock.mock_open' detected (F401).


[error] 22-22: Ruff check: unused import 'create_pathspec_from_gitignore' detected (F401).

🔇 Additional comments (10)
tests/cli/commands/test_deploy_command.py (5)

77-78: LGTM! Help text validation updated correctly.

The assertions properly verify that the new --ignore-file option and .mcpacignore references appear in the help output.


393-446: LGTM! Auto-detection test validates default ignore behavior.

The test correctly verifies that a .mcpacignore file in the project directory is automatically detected and passed to wrangler_deploy via the ignore_file parameter.


449-503: LGTM! No-ignore test validates fallback behavior.

The test correctly verifies that when no .mcpacignore exists and no --ignore-file is provided, None is passed to wrangler_deploy.


506-562: LGTM! Custom ignore-file test validates CLI option.

The test correctly verifies that an explicit --ignore-file path is resolved to an absolute path and passed through to wrangler_deploy.


565-622: LGTM! Precedence test validates override behavior.

The test correctly verifies that an explicit --ignore-file takes precedence over the default .mcpacignore detection, which matches the documented behavior.

tests/cli/commands/test_wrangler_wrapper.py (3)

926-1021: LGTM! Secrets exclusion test updated with example file handling.

The test now correctly verifies that:

  • The live mcp_agent.secrets.yaml is excluded from the bundle
  • The mcp_agent.secrets.yaml.example file is included and transformed to .mcpac.py
  • Other YAML files are processed normally

This aligns with the updated bundling behavior.


1024-1056: Verify Path.is_dir mocking approach.

The test monkey-patches Path.is_dir globally, which could affect other tests if exceptions occur. Consider using a context manager or patch to limit the scope.

Apply this refactor for safer mocking:

-    # Mock Path.is_dir method properly
-    original_is_dir = Path.is_dir
-    Path.is_dir = lambda self: self.name in ["node_modules", "temp", "build"]
-
-    try:
-        ignored = should_ignore_by_gitignore(current_path, names, project_dir, spec)
-    finally:
-        # Restore original method
-        Path.is_dir = original_is_dir
+    with patch.object(Path, 'is_dir', lambda self: self.name in ["node_modules", "temp", "build"]):
+        ignored = should_ignore_by_gitignore(current_path, names, project_dir, spec)

1058-1109: LGTM! Ignore-file integration test validates end-to-end behavior.

The test correctly verifies that:

  • Files matching ignore patterns are excluded from the temp directory
  • Files not matching patterns are copied and transformed to .mcpac.py
  • The ignore file is passed through to wrangler_deploy
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)

165-177: LGTM! Ignore spec loading and logging are clear.

The logic correctly:

  • Only loads ignore patterns when an explicit ignore file is provided
  • Warns if the ignore file doesn't exist
  • Logs the ignore source (file path or default excludes only)

232-250: LGTM! Bundled files logging improves transparency.

The new logging:

  • Computes and displays the list of original files being bundled
  • Excludes internal helpers (wrangler.toml, mcp_deploy_breadcrumb.py)
  • Maps .mcpac.py files back to their original names
  • Sorts the list for deterministic output

This greatly improves deployment observability.

Comment on lines +180 to 200
def ignore_patterns(path_str, names):
ignored = set()

# Keep existing hardcoded exclusions (highest priority)
for name in names:
if (name.startswith(".") and name not in {".env"}) or name in {
"logs",
"__pycache__",
"node_modules",
"venv",
MCP_SECRETS_FILENAME,
MCP_SECRETS_FILENAME, # Exclude mcp_agent.secrets.yaml only
}:
ignored.add(name)

# Apply explicit ignore file patterns (if provided)
spec_ignored = should_ignore_by_gitignore(
path_str, names, project_dir, ignore_spec
)
ignored.update(spec_ignored)

return ignored
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify .env exclusion for secret safety.

Line 185 whitelists .env by excluding all dotfiles except .env. This could leak secrets commonly stored in .env files.

Based on past review comments, this was flagged before. Should .env be excluded by default? If .env needs to be bundled for specific use cases, consider documenting why.


🏁 Script executed:

#!/bin/bash
# Check if .env is mentioned in documentation or tests as intentionally bundled
rg -n "\.env" docs/ README.md --type md -C2

Length of output: 760


🏁 Script executed:

#!/bin/bash
# Check if .env is in .gitignore (standard practice)
if [ -f .gitignore ]; then
  echo "=== .gitignore contents related to .env ==="
  rg -n "\.env" .gitignore
else
  echo ".gitignore not found"
fi

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Find and examine the should_ignore_by_gitignore function implementation
echo "=== Searching for should_ignore_by_gitignore function ==="
ast-grep --pattern 'def should_ignore_by_gitignore($$$) {
  $$$
}'

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Look for any tests or documentation about deployment exclusions
echo "=== Searching for deployment-related tests ==="
rg -n "ignore_patterns|\.env" --type py -g '*test*' -C3

Length of output: 14340


🏁 Script executed:

#!/bin/bash
# Check for any other references to .env handling in deployment code
echo "=== Searching for .env in deployment-related code ==="
rg -n "\.env" src/mcp_agent/cli/cloud/commands/deploy/ --type py -C2

Length of output: 1517


🏁 Script executed:

#!/bin/bash
# Get the full test context around line 412 to see what's actually being tested
echo "=== Full test context for .env handling ==="
rg -n "def test.*" tests/cli/commands/test_wrangler_wrapper.py -A 50 | rg -A 50 "Check that hidden files"

Length of output: 2543


🏁 Script executed:

#!/bin/bash
# Look for any assertion about .env in the test file
echo "=== Searching for .env assertions in wrangler tests ==="
rg -n "\.env" tests/cli/commands/test_wrangler_wrapper.py -B2 -A2

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Check if the test fixture creates a .env file
echo "=== Searching for .env in test fixtures and conftest ==="
rg -n "\.env" tests/cli/commands/test_wrangler_wrapper.py tests/cli/commands/conftest.py -B3 -A3

Length of output: 763


🏁 Script executed:

#!/bin/bash
# Look for the complex_project_structure fixture definition
echo "=== Finding complex_project_structure fixture ==="
ast-grep --pattern 'def complex_project_structure($$$):
  $$$'

Length of output: 4461


CRITICAL: Remove .env from deployment whitelist to prevent secret leakage.

Line 185 explicitly whitelists .env files for deployment by excluding all dotfiles except .env. This creates a critical security vulnerability:

  1. .env is gitignored, confirming it contains secrets not meant for version control
  2. README states .env is for local development and "In production, prefer MCP_APP_SETTINGS_PRELOAD to avoid writing plaintext secrets to disk"
  3. .env contains API keys like OPENAI_API_KEY and ANTHROPIC_API_KEY

If a developer has a local .env file (common during development), it will be bundled into the Cloudflare Workers deployment, exposing production secrets.

Required fix:

if name.startswith(".") or name in {
    "logs",
    "__pycache__",
    "node_modules", 
    "venv",
    MCP_SECRETS_FILENAME,
}:
    ignored.add(name)

Remove .env from the exception list—all dotfiles should be excluded from deployment.

🤖 Prompt for AI Agents
In src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py around lines 180
to 200, the current logic explicitly whitelists “.env” by excluding all dotfiles
except ".env", which risks bundling local secret files into deployments; change
the condition so that all dotfiles are treated as ignored (remove ".env" from
the exception list) and update the surrounding comment to reflect that all
dotfiles are excluded to prevent secret leakage; ensure the ignored.add(name)
branch triggers for any name that starts with "." (no exceptions) while
preserving the other hardcoded exclusions (logs, __pycache__, node_modules,
venv, MCP_SECRETS_FILENAME).

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: 0

🧹 Nitpick comments (1)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)

114-138: Enhance docstring with gitignore pattern details.

The ignore_file parameter description is present but could be more detailed. Consider expanding the docstring to explain gitignore pattern syntax and precedence with default excludes.

Apply this diff to enhance the docstring:

     """Bundle the MCP Agent using Wrangler.
 
     A thin wrapper around the Wrangler CLI to bundle the MCP Agent application code
     and upload it our internal cf storage.
+    
+    The `ignore_file` parameter supports gitignore-style patterns for excluding files 
+    and directories from the bundle. When provided, these patterns are evaluated in 
+    addition to the default hardcoded exclusions (dotfiles, logs, __pycache__, etc.).
 
     Some key details here:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 660ac95 and f35880b.

📒 Files selected for processing (2)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (5 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/cli/commands/test_wrangler_wrapper.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/utils/ux.py (2)
  • print_warning (66-77)
  • print_info (32-49)
🔇 Additional comments (4)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (4)

21-24: LGTM!

Clean imports for the new gitignore-style ignore functionality.


166-178: LGTM!

The ignore file loading logic correctly handles all cases (not provided, provided but missing, and provided and exists) with appropriate logging.


233-251: LGTM!

Excellent observability improvement. The bundled files logging provides clear visibility into what's being deployed, correctly excludes internal helpers, and properly handles renamed files. Sorted output ensures stable, reproducible logs.


181-201: CRITICAL: Remove .env from deployment bundle to prevent secret leakage.

Line 186 still whitelists .env files for deployment, creating a critical security vulnerability that was flagged in multiple past reviews. If a developer has a local .env file (common during development), it will be bundled into the Cloudflare Workers deployment, exposing production secrets like API keys.

The .env file:

  1. Is gitignored, confirming it contains secrets not meant for version control
  2. Contains API keys like OPENAI_API_KEY and ANTHROPIC_API_KEY (per README)
  3. Should never be deployed per README: "In production, prefer MCP_APP_SETTINGS_PRELOAD"

Apply this diff to exclude .env from deployments:

             # Keep existing hardcoded exclusions (highest priority)
             for name in names:
-                if (name.startswith(".") and name not in {".env"}) or name in {
+                if name.startswith(".") or name in {
                     "logs",
                     "__pycache__",
                     "node_modules",
                     "venv",
                     MCP_SECRETS_FILENAME,  # Exclude mcp_agent.secrets.yaml only
                 }:
                     ignored.add(name)

Comment on lines 64 to 76
rel_path = full_path.relative_to(project_dir)
# Normalize to POSIX separators so patterns work cross-platform (Windows too)
rel_path_str = rel_path.as_posix()

# Match files exactly; for directories also try with a trailing slash
# to respect patterns like `build/`.
if spec.match_file(rel_path_str):
ignored.add(name)
elif full_path.is_dir() and spec.match_file(rel_path_str + "/"):
ignored.add(name)
except ValueError:
# If `full_path` is not under `project_dir`, ignore matching is skipped.
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

The try/except block catches all ValueError exceptions, but it appears the intention is to only handle the specific case where relative_to() fails when a path isn't under the project directory. Consider narrowing the exception handling to immediately follow the relative_to() call:

try:
    rel_path = full_path.relative_to(project_dir)
except ValueError:
    # Skip paths not under project_dir
    continue

# Normalize to POSIX separators so patterns work cross-platform (Windows too)
rel_path_str = rel_path.as_posix()
# ...rest of the matching logic...

This would prevent silently ignoring other potential ValueError exceptions from the path manipulation or matching operations, making debugging easier.

Suggested change
rel_path = full_path.relative_to(project_dir)
# Normalize to POSIX separators so patterns work cross-platform (Windows too)
rel_path_str = rel_path.as_posix()
# Match files exactly; for directories also try with a trailing slash
# to respect patterns like `build/`.
if spec.match_file(rel_path_str):
ignored.add(name)
elif full_path.is_dir() and spec.match_file(rel_path_str + "/"):
ignored.add(name)
except ValueError:
# If `full_path` is not under `project_dir`, ignore matching is skipped.
continue
try:
rel_path = full_path.relative_to(project_dir)
except ValueError:
# If `full_path` is not under `project_dir`, ignore matching is skipped.
continue
# Normalize to POSIX separators so patterns work cross-platform (Windows too)
rel_path_str = rel_path.as_posix()
# Match files exactly; for directories also try with a trailing slash
# to respect patterns like `build/`.
if spec.match_file(rel_path_str):
ignored.add(name)
elif full_path.is_dir() and spec.match_file(rel_path_str + "/"):
ignored.add(name)

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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

♻️ Duplicate comments (1)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)

189-210: CRITICAL: Remove .env from whitelist to prevent secret leakage.

Line 195 explicitly whitelists .env by excluding all dotfiles except .env. This creates a critical security vulnerability:

  1. .env files commonly contain secrets (API keys, tokens, passwords)
  2. The project's .gitignore excludes .env, confirming it's sensitive
  3. README states .env is for local development and "In production, prefer MCP_APP_SETTINGS_PRELOAD to avoid writing plaintext secrets to disk"
  4. Example .env contains OPENAI_API_KEY and ANTHROPIC_API_KEY

If a developer has a local .env file during development, it will be bundled and deployed to Cloudflare Workers, exposing production secrets.

Past review comments flagged this exact issue, which was supposedly addressed in commit ce313b3, but the whitelist remains in the current code.

Apply this diff to remove the whitelist exception:

             # Keep existing hardcoded exclusions (highest priority)
             for name in names:
-                if (name.startswith(".") and name not in {".env"}) or name in {
+                if name.startswith(".") or name in {
                     "logs",
                     "__pycache__",
                     "node_modules",
                     "venv",
                     MCP_SECRETS_FILENAME,  # Exclude mcp_agent.secrets.yaml only
                 }:
                     ignored.add(name)
🧹 Nitpick comments (2)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

383-383: Parameter naming: ignore vs ignore_file inconsistency.

The parameter is named ignore: Optional[Path] here but called ignore_file at the call site (line 403) and in the downstream wrangler_deploy signature. While this works (positional matching), consistent naming improves readability.

Consider renaming to match the rest of the chain:

 async def _deploy_with_retry(
     app_id: str,
     api_key: str,
     project_dir: Path,
     mcp_app_client: MCPAppClient,
     retry_count: int,
-    ignore: Optional[Path],
+    ignore_file: Optional[Path],
 ):

And update the call:

         wrangler_deploy(
             app_id=app_id,
             api_key=api_key,
             project_dir=project_dir,
-            ignore_file=ignore,
+            ignore_file=ignore_file,
         )
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)

114-116: Good API extension: ignore_file parameter with sensible default.

The signature cleanly extends wrangler_deploy with an optional ignore_file parameter (defaulting to None for backward compatibility). The docstring mentions the new parameter, though it could be more detailed about precedence and gitignore syntax.

Consider expanding the docstring parameter section to mention gitignore syntax and precedence:

     Args:
         app_id (str): The application ID.
         api_key (str): User MCP Agent Cloud API key.
         project_dir (Path): The directory of the project to deploy.
-        ignore_file (Path | None): Optional path to a gitignore-style file for excluding files from the bundle.
+        ignore_file (Path | None): Optional path to a gitignore-style ignore file.
+            Patterns are evaluated relative to project_dir. When None, only hardcoded excludes apply.

Also applies to: 137-137

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae754fa and 94be33f.

📒 Files selected for processing (5)
  • pyproject.toml (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (5 hunks)
  • tests/cli/commands/test_deploy_command.py (3 hunks)
  • tests/cli/commands/test_wrangler_wrapper.py (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/cli/commands/test_deploy_command.py
  • pyproject.toml
🧰 Additional context used
🧬 Code graph analysis (3)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/utils/ux.py (2)
  • print_warning (66-77)
  • print_info (32-49)
tests/cli/commands/test_wrangler_wrapper.py (1)
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (2)
  • create_pathspec_from_gitignore (17-36)
  • should_ignore_by_gitignore (40-78)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
🔇 Additional comments (11)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

119-128: LGTM! Clean parameter definition with appropriate validation.

The ignore_file option is well-defined with sensible defaults (exists=False allows non-existent paths, resolve_path=True normalizes the path). The help text clearly describes the behavior.

tests/cli/commands/test_wrangler_wrapper.py (7)

10-10: LGTM! Clean import of new dependencies.

The imports of pathspec and bundle utilities are necessary for the new ignore-file tests and match the production code structure.

Also applies to: 21-24


944-975: Good coverage: secrets example file handling.

The test correctly verifies that .secrets.yaml.example files are processed as .mcpac.py (unlike live secrets files which are excluded). This ensures example files ship with the bundle for reference without exposing actual secrets.

Also applies to: 1014-1014


1024-1061: Strong test: gitignore matching covers files, directories, and extensions.

The test exercises the core filtering logic with a realistic PathSpec containing file globs (*.log, *.pyc) and directory patterns (node_modules/, build/). The mock of Path.is_dir enables testing directory-specific matching without creating actual directories.


1063-1090: Good coverage: PathSpec creation and missing-file fallback.

These tests verify the utility correctly parses ignore files into a PathSpec matcher and returns None for missing files (enabling callers to fall back to default behavior). The assertions confirm the matcher respects gitignore syntax.


1092-1147: Comprehensive: edge cases for directory patterns and nested paths.

The tests confirm:

  • None spec returns empty ignore set (no false positives)
  • Directory patterns with trailing slash match folders
  • Nested patterns are evaluated relative to project root

This coverage validates the cross-platform path normalization (POSIX separators) mentioned in the bundle_utils implementation.


1150-1206: Excellent end-to-end test: ignore-file integration.

This test creates a realistic project with included and excluded files, supplies a real .mcpacignore, and verifies the temp bundle contains only the expected files. The assertions prove the ignore spec is correctly wired into copytree.


1208-1253: Good negative test: missing ignore file warning.

The test verifies that when an ignore file is specified but missing, the code:

  1. Warns the user (via print_warning)
  2. Falls back to default excludes (no files incorrectly ignored)

The assertions confirm the warning message includes the path and "not found", providing clear feedback.

src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (3)

21-24: LGTM! Import of ignore utilities.

Clean import of the new bundle utilities module. These functions encapsulate the gitignore-based filtering logic.


175-187: Clear logging of ignore behavior.

The logging clearly distinguishes three cases:

  1. Ignore file provided and loaded successfully
  2. Ignore file provided but missing (warns and falls back)
  3. No ignore file provided (applies defaults only)

This provides excellent visibility for debugging bundling issues.


242-260: Excellent: Bundled files list for transparency.

Computing and logging the list of bundled original files (excluding internal helpers like wrangler.toml and mcp_deploy_breadcrumb.py) provides transparency into what's being deployed. The sorted output ensures deterministic logs for debugging.

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: 0

🧹 Nitpick comments (2)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

380-407: Document the new ignore parameter in the docstring.

The function signature added an ignore: Optional[Path] parameter, but the docstring's Args section doesn't document it.

     Args:
         app_id: The application ID
         api_key: API key for authentication
         project_dir: Directory containing the project files
         mcp_app_client: MCP App client for API calls
         retry_count: Number of retry attempts for deployment
+        ignore: Optional path to ignore file (gitignore syntax) for bundling
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1)

37-37: Remove trailing whitespace.

Line 37 has trailing whitespace that should be removed for code style consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94be33f and f21a84e.

📒 Files selected for processing (4)
  • docs/cli-reference.mdx (3 hunks)
  • docs/cloud/overview.mdx (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/cli-reference.mdx
  • docs/cloud/overview.mdx
🧰 Additional context used
🧬 Code graph analysis (1)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
🔇 Additional comments (5)
src/mcp_agent/cli/cloud/commands/deploy/main.py (2)

119-131: LGTM! Clear precedence documentation in help text.

The option is properly typed and the help text accurately documents the three-tier precedence order that matches the implementation.


340-348: Verify: Should explicit --ignore-file fail if the file doesn't exist?

Currently, if a user provides --ignore-file missing.txt that doesn't exist, the code silently falls through to auto-detecting .mcpacignore in config-dir or CWD. This could be confusing behavior.

Consider whether an explicitly provided ignore file that doesn't exist should:

  1. Fail fast with a clear error (user explicitly specified a file), or
  2. Silently continue with fallback (current behavior)

If option 1 is preferred, add a check after line 343:

if ignore_file is not None:
    if not ignore_file.exists():
        raise CLIError(
            f"Specified ignore file '{ignore_file}' does not exist.",
            retriable=False,
        )
    ignore_path = ignore_file
src/mcp_agent/cli/cloud/commands/deploy/bundle_utils.py (3)

17-36: LGTM! Clean implementation with proper encoding.

The function correctly handles non-existent files and uses explicit UTF-8 encoding for cross-platform compatibility.


40-80: LGTM! Correct gitignore semantics with cross-platform support.

The implementation correctly:

  • Handles None spec gracefully
  • Normalizes paths to POSIX format for cross-platform compatibility
  • Respects directory-with-trailing-slash patterns
  • Safely handles paths outside project_dir

12-14: No changes needed for pathspec import
All usages of bundle_utils reside in CLI-only modules (the deploy command and its tests), which already mandate installing the [cli] extras that include pathspec. Keeping the top-level import is acceptable.

@alienzach alienzach merged commit 75935f6 into main Sep 30, 2025
9 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 30, 2025
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.

3 participants