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
2 changes: 2 additions & 0 deletions smart_tests/commands/record/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .attachment import attachment
from .build import build
from .commit import commit
from .deployment import deployment
from .session import session
from .tests import tests

Expand All @@ -17,3 +18,4 @@ def record(app: Application):
record.add_command(tests)
record.add_command(session)
record.add_command(attachment)
record.add_command(deployment)
26 changes: 23 additions & 3 deletions smart_tests/commands/record/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def build(
metavar="NAME=BUILDNAME",
type=parse_key_value,
)] = [],
include_environment: Annotated[str | None, typer.Option(
"--include-environment",
help="Include all services deployed to this environment as components. See 'record deployment' command.",
metavar="NAME",
)] = None,
):

# Parse key-value pairs for commits
Expand Down Expand Up @@ -311,17 +316,32 @@ def compute_links():
return capture_links(link_options=links, env=os.environ)

def compute_components():
for c in components:
all_components = list(components)
if include_environment:
res = client.request("get", "builds/aliases",
params={"name": f"deployment:{include_environment}:*"})
res.raise_for_status()
json = res.json()
if len(json) == 0:
warn_and_exit_if_fail_fast_mode(f"Error: no such environment: {include_environment}")
click.echo("Components in this environment:")
for alias_obj in json:
service = alias_obj["alias"].split(":", 2)[2]
point_to = alias_obj["buildName"]
all_components.append(KeyValue(key=service, value=point_to))
click.echo(f" - {service} -> {point_to}")

for c in all_components:
if not c.key:
click.echo("Component name must not be empty", err=True)
raise typer.Exit(1)
names_seen: set = set()
for c in components:
for c in all_components:
if c.key in names_seen:
click.echo(f"Duplicate component name: '{c.key}'", err=True)
raise typer.Exit(1)
names_seen.add(c.key)
return [{"name": c.key, "build": c.value} for c in components]
return [{"name": c.key, "build": c.value} for c in all_components]

try:
lineage = branch or ws[0].branch
Expand Down
47 changes: 47 additions & 0 deletions smart_tests/commands/record/deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Annotated

import click

import smart_tests.args4p.typer as typer
from smart_tests import args4p
from smart_tests.app import Application
from smart_tests.utils.commands import Command
from smart_tests.utils.fail_fast_mode import set_fail_fast_mode, warn_and_exit_if_fail_fast_mode
from smart_tests.utils.smart_tests_client import SmartTestsClient
from smart_tests.utils.tracking import TrackingClient


@args4p.command(help="Record a deployment (sets alias deployment:{environment}:{service} -> build)")
def deployment(
app: Application,
build_name: Annotated[str, typer.Option(
"--build",
help="Build name",
metavar="NAME",
required=True,
)],
environment: Annotated[str, typer.Option(
"--environment",
help="Deployment environment name",
metavar="NAME",
required=True,
)],
service: Annotated[str, typer.Option(
"--service",
help="Service name",
metavar="NAME",
required=True,
)],
):
alias_name = f"deployment:{environment}:{service}"

tracking_client = TrackingClient(Command.RECORD_DEPLOYMENT, app=app)
client = SmartTestsClient(app=app, tracking_client=tracking_client)
set_fail_fast_mode(client.is_fail_fast_mode())

try:
res = client.request("put", f"builds/aliases/{alias_name}", payload={"build": build_name})
res.raise_for_status()
click.echo(f"Deployment of '{service}' in '{environment}' now points to build '{build_name}'")
except Exception as e:
warn_and_exit_if_fail_fast_mode(f"Failed to record deployment: {e}")
3 changes: 3 additions & 0 deletions smart_tests/utils/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Command(Enum):
DETECT_FLAKE = 'DETECT_FLAKE'
GATE = 'GATE'
UPDATE_ALIAS = 'UPDATE_ALIAS'
RECORD_DEPLOYMENT = 'RECORD_DEPLOYMENT'

# when you add a new constant here, the server also needs to get a new constant in cli_tracking.proto

def display_name(self):
return self.value.lower().replace('_', ' ')
46 changes: 46 additions & 0 deletions tests/commands/record/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,49 @@ def test_duplicate_component_name(self):
)
self.assert_exit_code(result, 1)
self.assertIn("Duplicate component name: 'payment'", result.output)

@responses.activate
@mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token})
# to tests on GitHub Actions
@mock.patch.dict(os.environ, {"GITHUB_ACTIONS": ""})
@mock.patch.dict(os.environ, {"GITHUB_PULL_REQUEST_URL": ""})
def test_include_environment(self):
from smart_tests.utils.http_client import get_base_url
aliases_url = (
f"{get_base_url()}/intake/organizations/{self.organization}"
f"/workspaces/{self.workspace}/builds/aliases"
)
responses.add(responses.GET, aliases_url, json=[
{"alias": "deployment:staging:payment", "buildName": "payment-main-137"},
{"alias": "deployment:staging:auth", "buildName": "auth-main-42"},
], status=200)

result = self.cli(
"record", "build",
"--build", self.build_name,
"--branch", "main",
"--no-commit-collection",
"--commit", ".=abc123",
"--repo-branch-map", ".=AIENG-404",
"--include-environment", "staging",
)
self.assert_success(result)

post_call = next(c for c in responses.calls if c.request.method == "POST" and "builds" in c.request.url)
payload = json.loads(post_call.request.body.decode())
self.assert_json_orderless_equal(
{
"buildNumber": "123",
"lineage": "main",
"commitHashes": [{"repositoryName": ".", "commitHash": "abc123", "branchName": "AIENG-404"}],
"links": [],
"timestamp": None,
"components": [
{"name": "payment", "build": "payment-main-137"},
{"name": "auth", "build": "auth-main-42"},
],
}, payload)

from urllib.parse import unquote
get_call = next(c for c in responses.calls if c.request.method == "GET" and "aliases" in c.request.url)
self.assertIn("deployment:staging:*", unquote(get_call.request.url))
59 changes: 59 additions & 0 deletions tests/commands/record/test_deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json
import os
from unittest import mock

import responses # type: ignore

from smart_tests.utils.http_client import get_base_url
from tests.cli_test_case import CliTestCase


class DeploymentTest(CliTestCase):
environment = "staging"
service = "payment"
build_name = "jenkins-main-135"
error_body = "Build 'jenkins-main-135' not found"

def deployment_url(self):
alias_name = f"deployment:{self.environment}:{self.service}"
return (
f"{get_base_url()}/intake/organizations/{self.organization}"
f"/workspaces/{self.workspace}/builds/aliases/{alias_name}"
)

@responses.activate
@mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token})
def test_record_deployment(self):
responses.add(responses.PUT, self.deployment_url(), json={}, status=200)

result = self.cli(
"record", "deployment",
"--build", self.build_name,
"--environment", self.environment,
"--service", self.service,
)
self.assert_success(result)

put_call = next(c for c in responses.calls if c.request.method == "PUT")
self.assert_json_orderless_equal(
{"build": self.build_name},
json.loads(put_call.request.body)
)
self.assertIn(self.service, result.output)
self.assertIn(self.environment, result.output)
self.assertIn(self.build_name, result.output)

@responses.activate
@mock.patch.dict(os.environ, {"SMART_TESTS_TOKEN": CliTestCase.smart_tests_token})
def test_record_deployment_build_not_found(self):
"""Without fail-fast mode: prints warning in yellow, exits 0 (doesn't halt CI)."""
responses.add(responses.PUT, self.deployment_url(), json={"reason": self.error_body}, status=404)

result = self.cli(
"record", "deployment",
"--build", self.build_name,
"--environment", self.environment,
"--service", self.service,
)
self.assert_success(result)
self.assertIn(self.error_body, result.output)
Loading