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
17 changes: 7 additions & 10 deletions src/deploydiff.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Dynamic: license-file
# DeployDiff CLI

[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/deploydiff?style=social)](https://github.com/Coding-Dev-Tools/deploydiff/stargazers)
[![Awesome DevOps](https://img.shields.io/badge/Awesome_DevOps-Submitted-grey?logo=github)](https://github.com/wmariuss/awesome-devops)<!-- PR #433 -->

Preview infrastructure changes with human-readable diffs, cost impact estimation, and rollback commands — before you hit deploy.

Expand Down Expand Up @@ -69,6 +70,12 @@ scoop bucket add Coding-Dev-Tools https://github.com/Coding-Dev-Tools/scoop-buck
scoop install deploydiff
```

**npm (Node.js wrapper):**
```bash
npm install -g deploydiff
```
Then run: `deploydiff --help`

## Usage

```bash
Expand Down Expand Up @@ -154,13 +161,3 @@ DeployDiff is one of eight tools in the Revenue Holdings suite. One license cove
## License

MIT



## Install via npm

```bash
npm install -g deploydiff
```

Then run: `deploydiff --help`
Binary file modified src/deploydiff/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/cli.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/cloudformation_parser.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/cost_estimator.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/diff_renderer.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/models.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/pulumi_parser.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/rollback.cpython-312.pyc
Binary file not shown.
Binary file modified src/deploydiff/__pycache__/terraform_parser.cpython-312.pyc
Binary file not shown.
30 changes: 28 additions & 2 deletions src/deploydiff/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ def main():
@click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file")
@click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file")
@click.option("-v", "--verbose", is_flag=True, help="Show before/after details for each change")
def preview(terraform_file, cloudformation_file, pulumi_file, verbose):
@click.option(
"--exit-on-destroy",
is_flag=True,
help="Exit with code 1 if the plan contains destructive changes (deletes or replaces)",
)
def preview(terraform_file, cloudformation_file, pulumi_file, verbose, exit_on_destroy):
"""Preview infrastructure changes from a plan file."""
plan = _load_plan(terraform_file, cloudformation_file, pulumi_file)
if plan is None:
Expand All @@ -45,13 +50,26 @@ def preview(terraform_file, cloudformation_file, pulumi_file, verbose):

render_plan(plan, console, verbose=verbose)

if exit_on_destroy and plan.destructive_changes:
console.print(
f"\n[red]Plan contains {len(plan.destructive_changes)} destructive change(s). "
f"Exiting with code 1 (--exit-on-destroy).[/red]"
)
raise SystemExit(1)


@main.command()
@click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file")
@click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file")
@click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file")
@click.option("--pricing", "pricing_file", type=click.Path(exists=True), help="Custom pricing JSON file")
def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file):
@click.option(
"--threshold",
type=float,
default=None,
help="Exit with code 1 if total monthly cost delta exceeds this value (e.g. 500 for $500)",
)
def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file, threshold):
"""Estimate monthly cost impact of infrastructure changes."""
plan = _load_plan(terraform_file, cloudformation_file, pulumi_file)
if plan is None:
Expand All @@ -61,6 +79,14 @@ def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file):
estimates = estimate_costs(plan, pricing_file=pricing_file)
_render_costs(estimates, plan, console)

if threshold is not None and plan.total_monthly_delta > threshold:
console.print(
f"\n[red]Total monthly cost increase of ${plan.total_monthly_delta:.2f} "
f"exceeds threshold of ${threshold:.2f}. "
f"Exiting with code 1 (--threshold).[/red]"
)
raise SystemExit(1)


@main.command()
@click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file")
Expand Down
Binary file modified tests/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file modified tests/__pycache__/test_deploydiff.cpython-312-pytest-9.0.3.pyc
Binary file not shown.
65 changes: 65 additions & 0 deletions tests/test_deploydiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,68 @@ def test_rollback_pulumi(self, sample_pulumi_preview, tmp_path):
runner = CliRunner()
result = runner.invoke(main, ["rollback", "--pulumi", str(pulumi_file)])
assert result.exit_code == 0

def test_preview_exit_on_destroy_no_destroy(self, tmp_path):
"""--exit-on-destroy exits 0 when plan has no destructive changes."""
# Plan with only creates and updates — no deletes/replaces
safe_plan = {
"format_version": "1.2",
"resource_changes": [
{
"address": "aws_instance.web",
"type": "aws_instance",
"name": "web",
"provider_name": "registry.terraform.io/hashicorp/aws",
"change": {
"actions": ["create"],
"before": None,
"after": {"instance_type": "t3.micro"},
},
},
{
"address": "aws_db_instance.primary",
"type": "aws_db_instance",
"name": "primary",
"provider_name": "registry.terraform.io/hashicorp/aws",
"change": {
"actions": ["update"],
"before": {"instance_class": "db.t3.small"},
"after": {"instance_class": "db.t3.medium"},
},
},
],
}
tf_file = tmp_path / "safe_plan.json"
tf_file.write_text(json.dumps(safe_plan))
runner = CliRunner()
result = runner.invoke(main, ["preview", "--tf", str(tf_file), "--exit-on-destroy"])
assert result.exit_code == 0

def test_preview_exit_on_destroy_with_destroy(self, sample_terraform_plan, tmp_path):
"""--exit-on-destroy exits 1 when plan has destructive changes (deletes/replaces)."""
tf_file = tmp_path / "plan.json"
tf_file.write_text(json.dumps(sample_terraform_plan))
runner = CliRunner()
# terraform fixture has a delete + replace (destructive)
result = runner.invoke(main, ["preview", "--tf", str(tf_file), "--exit-on-destroy"])
assert result.exit_code == 1
assert "destructive" in result.output.lower()

def test_cost_threshold_under(self, sample_terraform_plan, tmp_path):
"""--threshold exits 0 when delta is under the threshold."""
tf_file = tmp_path / "plan.json"
tf_file.write_text(json.dumps(sample_terraform_plan))
runner = CliRunner()
# Total delta for fixture is $6.50, so $1000 threshold should pass
result = runner.invoke(main, ["cost", "--tf", str(tf_file), "--threshold", "1000"])
assert result.exit_code == 0

def test_cost_threshold_exceeded(self, sample_terraform_plan, tmp_path):
"""--threshold exits 1 when delta exceeds the threshold."""
tf_file = tmp_path / "plan.json"
tf_file.write_text(json.dumps(sample_terraform_plan))
runner = CliRunner()
# Total delta for fixture is $6.50, so $1 threshold should trigger
result = runner.invoke(main, ["cost", "--tf", str(tf_file), "--threshold", "1"])
assert result.exit_code == 1
assert "threshold" in result.output.lower()