Skip to content
Closed
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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,8 @@ The blue-green deployment system includes automatic post-deployment steps that e
4. **Traffic Switch**: Switches load balancer traffic to new instances
5. **Scale Down Protection**: Resets ASG minimum sizes
6. **Compiler Routing Update**: Automatically updates the compiler routing table for the environment
7. **GitHub Notifications**: Sends notifications for production deployments (when enabled)
7. **CloudFront Invalidation**: Invalidates the environment's distributions (skip with `--skip-cloudfront`)
8. **GitHub Notifications**: Sends notifications for production deployments (when enabled)

### Compiler Routing Integration

Expand Down
9 changes: 9 additions & 0 deletions bin/lib/blue_green_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
set_version_for_deployment,
)
from lib.ce_utils import is_running_on_admin_node
from lib.cloudfront_utils import invalidate_cloudfront_distributions
from lib.compiler_routing import update_compiler_routing_table
from lib.deployment_utils import (
check_instance_health,
Expand Down Expand Up @@ -330,6 +331,7 @@ def deploy(
ignore_hash_mismatch: bool = False,
skip_compiler_check: bool = False,
compiler_timeout: int = 600,
skip_cloudfront: bool = False,
) -> None:
"""Perform a blue-green deployment with optional version setting."""
active_color = self.get_active_color()
Expand Down Expand Up @@ -593,6 +595,13 @@ def deploy(
LOGGER.warning("Deployment will continue, but compiler routing may be out of date")
print(f" ⚠️ Warning: Compiler routing update failed: {e}")

# Step 6.5: Invalidate CloudFront so the edge stops serving the old version's assets
if skip_cloudfront:
print("\nStep 6.5: Skipping CloudFront invalidation (--skip-cloudfront)")
else:
print("\nStep 6.5: Invalidating CloudFront caches")
invalidate_cloudfront_distributions(self.cfg)

print(f"\n✅ Blue-green deployment complete! Now serving from {inactive_color}")
print(f"Old {active_color} ASG remains running for rollback if needed")

Expand Down
3 changes: 3 additions & 0 deletions bin/lib/cli/blue_green.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def blue_green_status(cfg: Config, detailed: bool):
default=600,
help="Timeout in seconds for compiler registration check (default: 600)",
)
@click.option("--skip-cloudfront", is_flag=True, help="Skip CloudFront invalidation after switching traffic")
@click.argument("version", required=False)
@click.pass_obj
def blue_green_deploy(
Expand All @@ -222,6 +223,7 @@ def blue_green_deploy(
ignore_hash_mismatch: bool,
skip_compiler_check: bool,
compiler_timeout: int,
skip_cloudfront: bool,
version: str | None,
):
"""Deploy to the inactive color using blue-green strategy.
Expand Down Expand Up @@ -351,6 +353,7 @@ def blue_green_deploy(
ignore_hash_mismatch=ignore_hash_mismatch,
skip_compiler_check=skip_compiler_check,
compiler_timeout=compiler_timeout,
skip_cloudfront=skip_cloudfront,
)
print("\nDeployment successful!")
print("Run 'ce blue-green rollback' if you need to revert")
Expand Down
70 changes: 70 additions & 0 deletions bin/test/blue_green_cloudfront_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Tests for CloudFront invalidation during blue-green deployments."""

from __future__ import annotations

import unittest
from unittest.mock import MagicMock, patch

import lib.cli # noqa: F401 (must import before blue_green_deploy to avoid circular import)
from lib.blue_green_deploy import BlueGreenDeployment
from lib.env import Config, Environment


class TestDeployCloudFrontInvalidation(unittest.TestCase):
"""The deploy flow invalidates CloudFront after the compiler routing update."""

def _run_deploy(self, skip_cloudfront: bool) -> tuple[MagicMock, MagicMock, list[str]]:
with patch("lib.blue_green_deploy.ssm_client"), patch("lib.blue_green_deploy.is_running_on_admin_node"):
deployment = BlueGreenDeployment(Config(env=Environment.PROD))
deployment.running_on_admin_node = False

with (
patch.object(deployment, "get_active_color", return_value="blue"),
patch.object(deployment, "get_inactive_color", return_value="green"),
patch.object(deployment, "get_asg_name", side_effect=lambda color: f"prod-{color}"),
patch.object(deployment, "switch_target_group") as mock_switch,
patch("lib.blue_green_deploy.get_asg_info", return_value={"DesiredCapacity": 0}),
patch("lib.blue_green_deploy.protect_asg_capacity", return_value=(1, 4)),
patch("lib.blue_green_deploy.scale_asg"),
patch("lib.blue_green_deploy.wait_for_instances_healthy", return_value=["i-1"]),
patch("lib.blue_green_deploy.clear_router_cache", return_value=True),
patch("lib.blue_green_deploy.reset_asg_min_size"),
patch("lib.blue_green_deploy.restore_asg_capacity_protection"),
patch("lib.blue_green_deploy.get_instance_private_ip", return_value="10.0.0.1"),
patch("lib.blue_green_deploy.update_compiler_routing_table") as mock_routing,
patch("lib.blue_green_deploy.invalidate_cloudfront_distributions") as mock_invalidate,
patch("builtins.print"),
):
manager = MagicMock()
manager.attach_mock(mock_switch, "switch")
manager.attach_mock(mock_routing, "routing")
manager.attach_mock(mock_invalidate, "cloudfront")
mock_routing.return_value = {"added": 0, "updated": 0, "deleted": 0}

deployment.deploy(
target_capacity=1,
skip_confirmation=True,
skip_compiler_check=True,
skip_cloudfront=skip_cloudfront,
)

order = [name for name, _args, _kwargs in manager.mock_calls]

return mock_switch, mock_invalidate, order

def test_invalidates_after_routing_update(self):
mock_switch, mock_invalidate, order = self._run_deploy(skip_cloudfront=False)
mock_switch.assert_called_once_with("green")
mock_invalidate.assert_called_once()
assert mock_invalidate.call_args[0][0].env == Environment.PROD
assert order == ["switch", "routing", "cloudfront"]

def test_skip_cloudfront_suppresses_invalidation(self):
mock_switch, mock_invalidate, order = self._run_deploy(skip_cloudfront=True)
mock_switch.assert_called_once_with("green")
mock_invalidate.assert_not_called()
assert order == ["switch", "routing"]


if __name__ == "__main__":
unittest.main()