Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
Compare deployment configurations across environments. Detect drift between staging and production configs. Preview infrastructure changes with human-readable diffs, cost impact estimation, and rollback commands.

## Build & Test Commands
- Install: `pip install -e .` or `pip install deploydiff`
- Install (editable, from this repo): `pip install -e .`
- Install (prebuilt wheel from the self-hosted index): `pip install --index-url https://coding-dev-tools.github.io/pypi-index/simple/ deploydiff`
- Install (from source): `pip install git+https://github.com/Coding-Dev-Tools/deploydiff.git`
- NOTE: `deploydiff` is NOT on public PyPI — use the self-hosted index or a `git+` URL above.
- Test: `pytest tests/` (or `python -m pytest tests/ -v --tb=short`)
- Lint: `ruff check .`
- Build: `pip install build twine && python -m build && twine check dist/*`
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@ Preview infrastructure changes with human-readable diffs, cost impact estimation
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Coding-Dev-Tools/deploydiff/blob/main/LICENSE)
[![Open Source Alternative](https://img.shields.io/badge/Open_Source_Alternative-%E2%87%92-blue?logo=opensourceinitiative)](https://www.opensourcealternative.to/project/deploydiff)
|[![LibHunt](https://img.shields.io/badge/LibHunt-%E2%87%92-blue?logo=codeigniter)](https://www.libhunt.com/r/Coding-Dev-Tools/deploydiff)
|[![PyPI](https://img.shields.io/pypi/v/deploydiff)](https://pypi.org/project/deploydiff/)
|[![PyPI](https://img.shields.io/badge/PyPI-not%20published-orange)](https://github.com/Coding-Dev-Tools/deploydiff#installation)|

## Installation

```bash
pip install deploydiff
```
DeployDiff is not published on public PyPI (publishing is pending). Install directly from GitHub:

Or install the latest version directly from GitHub:
```bash
pip install git+https://github.com/Coding-Dev-Tools/deploydiff.git
```
Expand Down
2 changes: 1 addition & 1 deletion src/deploydiff/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def mcp() -> None:
from .mcp_server import run_for_app
except ImportError as exc:
console.print(
"[red]Error: click-to-mcp is not installed.[/red]\nInstall it with: [bold]pip install click-to-mcp[/bold]"
"[red]Error: click-to-mcp is not installed.[/red]\nInstall it with: [bold]pip install git+https://github.com/Coding-Dev-Tools/click-to-mcp.git[/bold]"
)
raise SystemExit(1) from exc

Expand Down
2 changes: 0 additions & 2 deletions src/deploydiff/diff_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ def _render_action_group(
for change in changes:
symbol = change.display_action
addr = change.address
if change.module_path:
addr = f"{change.module_path}.{addr}"
table.add_row(
f"[{color}]{symbol}[/{color}]",
f"[{color}]{addr}[/{color}]",
Expand Down
4 changes: 2 additions & 2 deletions src/deploydiff/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def run_mcp() -> None:
import sys

print(
"Error: click-to-mcp is not installed. Install it with: pip install click-to-mcp",
"Error: click-to-mcp is not installed. Install it with: pip install git+https://github.com/Coding-Dev-Tools/click-to-mcp.git",
file=sys.stderr,
)
sys.exit(1)
Expand All @@ -37,7 +37,7 @@ def run_for_app(app: object) -> None:
import sys

print(
"Error: click-to-mcp is not installed. Install it with: pip install click-to-mcp",
"Error: click-to-mcp is not installed. Install it with: pip install git+https://github.com/Coding-Dev-Tools/click-to-mcp.git",
file=sys.stderr,
)
sys.exit(1)
Expand Down
85 changes: 84 additions & 1 deletion tests/test_deploydiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,10 +1182,93 @@ def test_missing_urn_in_step(self):
assert len(plan.changes) == 1

def test_mcp_without_click_to_mcp(self):
"""MCP command exits 1 when click-to-mcp is not installed."""
"MCP command exits 1 when click-to-mcp is not installed."
runner = CliRunner()
result = runner.invoke(main, ["mcp"])
# Either exits 1 (ImportError caught) or 0 (if click-to-mcp is installed)
assert result.exit_code in (0, 1)
if result.exit_code == 1:
assert "click-to-mcp" in result.output.lower()


class TestDiffRendererEdgeCases:
"Targeted tests for diff_renderer edge cases and module_path behavior."

def test_render_module_path_not_doubled(self):
"Module path should NOT be doubled in the rendered address."
from io import StringIO

from rich.console import Console

from deploydiff.diff_renderer import _render_action_group

change = ResourceChange(
address="module.vpc.aws_nat_gateway.main",
action=ChangeAction.REPLACE,
resource_type="aws_nat_gateway",
resource_name="main",
source=ChangeSource.TERRAFORM,
module_path="module.vpc",
)
plan = DeployPlan(source=ChangeSource.TERRAFORM, changes=[change])
buf = StringIO()
console = Console(file=buf, force_terminal=True)
_render_action_group(plan, ChangeAction.REPLACE, [change], console, verbose=False)
output = buf.getvalue()
# The rendered address should appear exactly once, not doubled
assert "module.vpc.aws_nat_gateway.main" in output
# The doubled form would be "module.vpc.module.vpc.aws_nat_gateway.main"
doubled = "module.vpc.module.vpc"
assert doubled not in output, f"Address doubled: {output[:500]}"

def test_render_change_details_unchanged_value(self):
"Keys with same before/after value show without diff markers."
from io import StringIO

from rich.console import Console

from deploydiff.diff_renderer import _render_change_details

change = ResourceChange(
address="aws_instance.web",
action=ChangeAction.UPDATE,
resource_type="aws_instance",
resource_name="web",
source=ChangeSource.TERRAFORM,
before={"instance_type": "t3.micro", "ami": "ami-old"},
after={"instance_type": "t3.micro", "ami": "ami-new"},
)
buf = StringIO()
console = Console(file=buf, force_terminal=True)
_render_change_details(change, console)
output = buf.getvalue()
assert "instance_type" in output
# t3.micro is unchanged, should appear without +/- markers
assert "t3.micro" in output

def test_render_change_details_missing_key(self):
"Key present in one state but not the other uses em-dash fallback."
from io import StringIO

from rich.console import Console

from deploydiff.diff_renderer import _render_change_details

change = ResourceChange(
address="aws_instance.web",
action=ChangeAction.UPDATE,
resource_type="aws_instance",
resource_name="web",
source=ChangeSource.TERRAFORM,
before={"instance_type": "t3.micro", "old_key": "old_val"},
after={"instance_type": "t3.large", "new_key": "new_val"},
)
buf = StringIO()
console = Console(file=buf, force_terminal=True)
_render_change_details(change, console)
output = buf.getvalue()
assert "instance_type" in output
# old_key should show "old_val" on the before side, em-dash on after
assert "old_key" in output
# Use some assertion that verifies em-dash appears (Rich renders these as Unicode)
assert "new_key" in output
Loading