devservices 1.0.6__tar.gz → 1.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {devservices-1.0.6 → devservices-1.0.8}/PKG-INFO +3 -1
  2. {devservices-1.0.6 → devservices-1.0.8}/README.md +1 -1
  3. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/down.py +4 -4
  4. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/logs.py +1 -1
  5. devservices-1.0.8/devservices/commands/purge.py +94 -0
  6. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/status.py +19 -9
  7. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/up.py +34 -8
  8. {devservices-1.0.6 → devservices-1.0.8}/devservices/configs/service_config.py +1 -1
  9. {devservices-1.0.6 → devservices-1.0.8}/devservices/constants.py +2 -1
  10. {devservices-1.0.6 → devservices-1.0.8}/devservices/exceptions.py +11 -0
  11. {devservices-1.0.6 → devservices-1.0.8}/devservices/main.py +0 -9
  12. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/dependencies.py +49 -0
  13. devservices-1.0.8/devservices/utils/docker.py +219 -0
  14. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/docker_compose.py +44 -5
  15. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/file_lock.py +1 -1
  16. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/PKG-INFO +3 -1
  17. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/requires.txt +2 -0
  18. {devservices-1.0.6 → devservices-1.0.8}/pyproject.toml +3 -1
  19. devservices-1.0.8/tests/commands/test_purge.py +517 -0
  20. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_status.py +75 -1
  21. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_up.py +275 -2
  22. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_dependencies.py +48 -0
  23. devservices-1.0.8/tests/utils/test_docker.py +493 -0
  24. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_docker_compose.py +165 -99
  25. devservices-1.0.6/devservices/commands/purge.py +0 -78
  26. devservices-1.0.6/devservices/utils/docker.py +0 -88
  27. devservices-1.0.6/tests/commands/test_purge.py +0 -214
  28. devservices-1.0.6/tests/utils/test_docker.py +0 -182
  29. {devservices-1.0.6 → devservices-1.0.8}/LICENSE.md +0 -0
  30. {devservices-1.0.6 → devservices-1.0.8}/devservices/__init__.py +0 -0
  31. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/__init__.py +0 -0
  32. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/list_dependencies.py +0 -0
  33. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/list_services.py +0 -0
  34. {devservices-1.0.6 → devservices-1.0.8}/devservices/commands/update.py +0 -0
  35. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/__init__.py +0 -0
  36. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/check_for_update.py +0 -0
  37. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/console.py +0 -0
  38. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/devenv.py +0 -0
  39. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/install_binary.py +0 -0
  40. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/services.py +0 -0
  41. {devservices-1.0.6 → devservices-1.0.8}/devservices/utils/state.py +0 -0
  42. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/SOURCES.txt +0 -0
  43. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/dependency_links.txt +0 -0
  44. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/entry_points.txt +0 -0
  45. {devservices-1.0.6 → devservices-1.0.8}/devservices.egg-info/top_level.txt +0 -0
  46. {devservices-1.0.6 → devservices-1.0.8}/setup.cfg +0 -0
  47. {devservices-1.0.6 → devservices-1.0.8}/testing/__init__.py +0 -0
  48. {devservices-1.0.6 → devservices-1.0.8}/testing/utils.py +0 -0
  49. {devservices-1.0.6 → devservices-1.0.8}/tests/__init__.py +0 -0
  50. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_down.py +0 -0
  51. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_list_dependencies.py +0 -0
  52. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_list_services.py +0 -0
  53. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_logs.py +0 -0
  54. {devservices-1.0.6 → devservices-1.0.8}/tests/commands/test_update.py +0 -0
  55. {devservices-1.0.6 → devservices-1.0.8}/tests/configs/test_service_config.py +0 -0
  56. {devservices-1.0.6 → devservices-1.0.8}/tests/conftest.py +0 -0
  57. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_check_for_update.py +0 -0
  58. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_install_binary.py +0 -0
  59. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_services.py +0 -0
  60. {devservices-1.0.6 → devservices-1.0.8}/tests/utils/test_state.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
7
7
  Requires-Dist: sentry-devenv
8
+ Requires-Dist: sentry-sdk
9
+ Requires-Dist: packaging
8
10
  Provides-Extra: dev
9
11
  Requires-Dist: black; extra == "dev"
10
12
  Requires-Dist: mypy; extra == "dev"
@@ -11,7 +11,7 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
11
11
  The recommended way to install devservices is through a virtualenv in the requirements.txt.
12
12
 
13
13
  ```
14
- devservices==1.0.6
14
+ devservices==1.0.8
15
15
  ```
16
16
 
17
17
 
@@ -14,7 +14,6 @@ from devservices.constants import DEPENDENCY_CONFIG_VERSION
14
14
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
- from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
18
17
  from devservices.exceptions import ConfigError
19
18
  from devservices.exceptions import DependencyError
20
19
  from devservices.exceptions import DockerComposeError
@@ -24,6 +23,7 @@ from devservices.utils.console import Status
24
23
  from devservices.utils.dependencies import get_non_shared_remote_dependencies
25
24
  from devservices.utils.dependencies import install_and_verify_dependencies
26
25
  from devservices.utils.dependencies import InstalledRemoteDependency
26
+ from devservices.utils.docker_compose import DockerComposeCommand
27
27
  from devservices.utils.docker_compose import get_docker_compose_commands_to_run
28
28
  from devservices.utils.docker_compose import run_cmd
29
29
  from devservices.utils.services import find_matching_service
@@ -111,12 +111,12 @@ def down(args: Namespace) -> None:
111
111
 
112
112
 
113
113
  def _bring_down_dependency(
114
- cmd: list[str], current_env: dict[str, str], status: Status
114
+ cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
115
115
  ) -> subprocess.CompletedProcess[str]:
116
116
  # TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
117
- for dependency in cmd[DOCKER_COMPOSE_COMMAND_LENGTH:]:
117
+ for dependency in cmd.services:
118
118
  status.info(f"Stopping {dependency}")
119
- return run_cmd(cmd, current_env)
119
+ return run_cmd(cmd.full_command, current_env)
120
120
 
121
121
 
122
122
  def _down(
@@ -114,7 +114,7 @@ def _logs(
114
114
 
115
115
  with concurrent.futures.ThreadPoolExecutor() as executor:
116
116
  futures = [
117
- executor.submit(run_cmd, cmd, current_env)
117
+ executor.submit(run_cmd, cmd.full_command, current_env)
118
118
  for cmd in docker_compose_commands
119
119
  ]
120
120
  for future in concurrent.futures.as_completed(futures):
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from argparse import _SubParsersAction
6
+ from argparse import ArgumentParser
7
+ from argparse import Namespace
8
+
9
+ from devservices.constants import DEVSERVICES_CACHE_DIR
10
+ from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11
+ from devservices.constants import DOCKER_NETWORK_NAME
12
+ from devservices.exceptions import DockerDaemonNotRunningError
13
+ from devservices.exceptions import DockerError
14
+ from devservices.utils.console import Console
15
+ from devservices.utils.console import Status
16
+ from devservices.utils.docker import get_matching_containers
17
+ from devservices.utils.docker import get_matching_networks
18
+ from devservices.utils.docker import get_volumes_for_containers
19
+ from devservices.utils.docker import remove_docker_resources
20
+ from devservices.utils.docker import stop_containers
21
+ from devservices.utils.state import State
22
+
23
+
24
+ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
25
+ parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
26
+ parser.set_defaults(func=purge)
27
+
28
+
29
+ def purge(_args: Namespace) -> None:
30
+ """Purge the local devservices state and cache and remove all devservices containers and volumes."""
31
+ console = Console()
32
+
33
+ if os.path.exists(DEVSERVICES_CACHE_DIR):
34
+ try:
35
+ shutil.rmtree(DEVSERVICES_CACHE_DIR)
36
+ except PermissionError as e:
37
+ console.failure(f"Failed to purge cache: {e}")
38
+ exit(1)
39
+ state = State()
40
+ state.clear_state()
41
+
42
+ try:
43
+ devservices_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
44
+ except DockerDaemonNotRunningError as e:
45
+ console.warning(str(e))
46
+ return
47
+ except DockerError as de:
48
+ console.failure(f"Failed to get devservices containers {de.stderr}")
49
+ exit(1)
50
+
51
+ try:
52
+ devservices_volumes = get_volumes_for_containers(devservices_containers)
53
+ except DockerError as e:
54
+ console.failure(f"Failed to get devservices volumes {e.stderr}")
55
+ exit(1)
56
+
57
+ with Status(
58
+ lambda: console.warning("Stopping all devservices containers"),
59
+ lambda: console.success("All devservices containers have been stopped"),
60
+ ):
61
+ try:
62
+ stop_containers(devservices_containers, should_remove=True)
63
+ except DockerError as e:
64
+ console.failure(f"Failed to stop devservices containers {e.stderr}")
65
+ exit(1)
66
+
67
+ console.warning("Removing any devservices docker volumes")
68
+ if len(devservices_volumes) == 0:
69
+ console.success("No devservices volumes found to remove")
70
+ else:
71
+ try:
72
+ remove_docker_resources("volume", list(devservices_volumes))
73
+ console.success("All devservices volumes removed")
74
+ except DockerError as e:
75
+ # We don't want to exit here since we still want to try to remove the networks
76
+ console.failure(f"Failed to remove devservices volumes {e.stderr}")
77
+
78
+ console.warning("Removing any devservices networks")
79
+ try:
80
+ devservices_networks = get_matching_networks(DOCKER_NETWORK_NAME)
81
+ except DockerError as e:
82
+ console.failure(f"Failed to get devservices networks {e.stderr}")
83
+ exit(1)
84
+ if len(devservices_networks) == 0:
85
+ console.success("No devservices networks found to remove")
86
+ else:
87
+ try:
88
+ remove_docker_resources("network", devservices_networks)
89
+ console.success("All devservices networks removed")
90
+ except DockerError as e:
91
+ console.failure(f"Failed to remove devservices networks {e.stderr}")
92
+ exit(1)
93
+
94
+ console.success("The local devservices cache and state has been purged")
@@ -7,6 +7,7 @@ import subprocess
7
7
  from argparse import _SubParsersAction
8
8
  from argparse import ArgumentParser
9
9
  from argparse import Namespace
10
+ from collections import namedtuple
10
11
 
11
12
  from sentry_sdk import capture_exception
12
13
 
@@ -30,6 +31,9 @@ from devservices.utils.services import Service
30
31
  LINE_LENGTH = 40
31
32
 
32
33
 
34
+ ServiceStatus = namedtuple("ServiceStatus", ["name", "formatted_output"])
35
+
36
+
33
37
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
34
38
  parser = subparsers.add_parser("status", help="View status of a service")
35
39
  parser.add_argument(
@@ -41,12 +45,12 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
41
45
  parser.set_defaults(func=status)
42
46
 
43
47
 
44
- def format_status_output(status_json: str) -> str:
48
+ def format_status_output(service_status_json: str) -> list[ServiceStatus]:
45
49
  # Docker compose ps is line delimited json, so this constructs this into an array we can use
46
- service_statuses = status_json.split("\n")[:-1]
47
- output = []
48
- output.append("-" * LINE_LENGTH)
50
+ service_statuses = service_status_json.split("\n")[:-1]
51
+ outputs = []
49
52
  for service_status in service_statuses:
53
+ output = []
50
54
  service = json.loads(service_status)
51
55
  name = service["Service"]
52
56
  state = service["State"]
@@ -71,8 +75,9 @@ def format_status_output(status_json: str) -> str:
71
75
  output.append("No ports exposed")
72
76
 
73
77
  output.append("") # Empty line for readability
78
+ outputs.append(ServiceStatus(name=name, formatted_output="\n".join(output)))
74
79
 
75
- return "\n".join(output)
80
+ return outputs
76
81
 
77
82
 
78
83
  def status(args: Namespace) -> None:
@@ -115,10 +120,15 @@ def status(args: Namespace) -> None:
115
120
  console.warning(f"{service.name} is not running")
116
121
  return
117
122
  output = f"Service: {service.name}\n\n"
123
+ output += "=" * LINE_LENGTH + "\n"
124
+ formatted_status_outputs = []
118
125
  for status_json in status_json_results:
119
- output += format_status_output(status_json.stdout)
120
- output += "=" * LINE_LENGTH
121
- console.info(output + "\n")
126
+ formatted_status_outputs.extend(format_status_output(status_json.stdout))
127
+ formatted_status_outputs.sort(key=lambda x: x.name)
128
+ for formatted_status_output in formatted_status_outputs:
129
+ output += formatted_status_output[1]
130
+ output += "-" * LINE_LENGTH + "\n"
131
+ console.info(output)
122
132
 
123
133
 
124
134
  def _status(
@@ -152,7 +162,7 @@ def _status(
152
162
 
153
163
  with concurrent.futures.ThreadPoolExecutor() as executor:
154
164
  futures = [
155
- executor.submit(run_cmd, cmd, current_env)
165
+ executor.submit(run_cmd, cmd.full_command, current_env)
156
166
  for cmd in docker_compose_commands
157
167
  ]
158
168
  for future in concurrent.futures.as_completed(futures):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import concurrent.futures
3
4
  import os
4
5
  import subprocess
5
6
  from argparse import _SubParsersAction
@@ -13,8 +14,8 @@ from devservices.constants import DEPENDENCY_CONFIG_VERSION
13
14
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
14
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
15
16
  from devservices.constants import DEVSERVICES_DIR_NAME
16
- from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
17
17
  from devservices.exceptions import ConfigError
18
+ from devservices.exceptions import ContainerHealthcheckFailedError
18
19
  from devservices.exceptions import DependencyError
19
20
  from devservices.exceptions import DockerComposeError
20
21
  from devservices.exceptions import ModeDoesNotExistError
@@ -24,6 +25,9 @@ from devservices.utils.console import Status
24
25
  from devservices.utils.dependencies import construct_dependency_graph
25
26
  from devservices.utils.dependencies import install_and_verify_dependencies
26
27
  from devservices.utils.dependencies import InstalledRemoteDependency
28
+ from devservices.utils.docker import check_all_containers_healthy
29
+ from devservices.utils.docker_compose import DockerComposeCommand
30
+ from devservices.utils.docker_compose import get_container_names_for_project
27
31
  from devservices.utils.docker_compose import get_docker_compose_commands_to_run
28
32
  from devservices.utils.docker_compose import run_cmd
29
33
  from devservices.utils.services import find_matching_service
@@ -101,12 +105,11 @@ def up(args: Namespace) -> None:
101
105
 
102
106
 
103
107
  def _bring_up_dependency(
104
- cmd: list[str], current_env: dict[str, str], status: Status, len_options: int
108
+ cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
105
109
  ) -> subprocess.CompletedProcess[str]:
106
- # TODO: Get rid of these constants, we need a smarter way to determine the containers being brought up
107
- for dependency in cmd[DOCKER_COMPOSE_COMMAND_LENGTH:-len_options]:
110
+ for dependency in cmd.services:
108
111
  status.info(f"Starting {dependency}")
109
- return run_cmd(cmd, current_env)
112
+ return run_cmd(cmd.full_command, current_env)
110
113
 
111
114
 
112
115
  def _up(
@@ -128,7 +131,6 @@ def _up(
128
131
  current_env[
129
132
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
130
133
  ] = relative_local_dependency_directory
131
- options = ["-d"]
132
134
  dependency_graph = construct_dependency_graph(service, modes=modes)
133
135
  starting_order = dependency_graph.get_starting_order()
134
136
  sorted_remote_dependencies = sorted(
@@ -139,13 +141,36 @@ def _up(
139
141
  remote_dependencies=sorted_remote_dependencies,
140
142
  current_env=current_env,
141
143
  command="up",
142
- options=options,
144
+ options=["-d", "--pull", "always"],
143
145
  service_config_file_path=service_config_file_path,
144
146
  mode_dependencies=mode_dependencies,
145
147
  )
146
148
 
149
+ containers_to_check = []
150
+ with concurrent.futures.ThreadPoolExecutor() as dependency_executor:
151
+ futures = [
152
+ dependency_executor.submit(_bring_up_dependency, cmd, current_env, status)
153
+ for cmd in docker_compose_commands
154
+ ]
155
+ for future in concurrent.futures.as_completed(futures):
156
+ _ = future.result()
157
+
147
158
  for cmd in docker_compose_commands:
148
- _bring_up_dependency(cmd, current_env, status, len(options))
159
+ try:
160
+ container_names = get_container_names_for_project(
161
+ cmd.project_name, cmd.config_path
162
+ )
163
+ containers_to_check.extend(container_names)
164
+ except DockerComposeError as dce:
165
+ status.failure(
166
+ f"Failed to get containers to healthcheck for {cmd.project_name}: {dce.stderr}"
167
+ )
168
+ exit(1)
169
+ try:
170
+ check_all_containers_healthy(status, containers_to_check)
171
+ except ContainerHealthcheckFailedError as e:
172
+ status.failure(str(e))
173
+ exit(1)
149
174
 
150
175
 
151
176
  def _create_devservices_network() -> None:
@@ -153,4 +178,5 @@ def _create_devservices_network() -> None:
153
178
  ["docker", "network", "create", "devservices"],
154
179
  stdout=subprocess.DEVNULL,
155
180
  stderr=subprocess.DEVNULL,
181
+ check=True,
156
182
  )
@@ -68,7 +68,7 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
68
68
  config_path = os.path.join(repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME)
69
69
  if not os.path.exists(config_path):
70
70
  raise ConfigNotFoundError(f"Config file not found in directory: {config_path}")
71
- with open(config_path, "r") as stream:
71
+ with open(config_path, "r", encoding="utf-8") as stream:
72
72
  try:
73
73
  config = yaml.safe_load(stream)
74
74
  except yaml.YAMLError as yml_error:
@@ -14,7 +14,6 @@ DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "depend
14
14
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
15
15
  STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
16
16
  DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
17
- DOCKER_COMPOSE_COMMAND_LENGTH = 7
18
17
 
19
18
  DEPENDENCY_CONFIG_VERSION = "v1"
20
19
  DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
@@ -38,3 +37,5 @@ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
38
37
  DEVSERVICES_CACHE_DIR, "latest_version.txt"
39
38
  )
40
39
  DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
40
+ HEALTHCHECK_TIMEOUT = 45
41
+ HEALTHCHECK_INTERVAL = 5
@@ -127,3 +127,14 @@ class FailedToSetGitConfigError(GitConfigError):
127
127
  """Raised when a git config cannot be set."""
128
128
 
129
129
  pass
130
+
131
+
132
+ class ContainerHealthcheckFailedError(Exception):
133
+ """Raised when a container is not healthy."""
134
+
135
+ def __init__(self, container_name: str, timeout: int):
136
+ self.container_name = container_name
137
+ self.timeout = timeout
138
+
139
+ def __str__(self) -> str:
140
+ return f"Container {self.container_name} did not become healthy within {self.timeout} seconds."
@@ -26,7 +26,6 @@ from devservices.commands import update
26
26
  from devservices.constants import LOGGER_NAME
27
27
  from devservices.exceptions import DockerComposeInstallationError
28
28
  from devservices.exceptions import DockerDaemonNotRunningError
29
- from devservices.utils.check_for_update import check_for_update
30
29
  from devservices.utils.console import Console
31
30
  from devservices.utils.docker_compose import check_docker_compose_version
32
31
 
@@ -102,14 +101,6 @@ def main() -> None:
102
101
  else:
103
102
  parser.print_help()
104
103
 
105
- if args.command != "update" and os.environ.get("CI") != "true":
106
- newest_version = check_for_update()
107
- if newest_version != current_version:
108
- console.warning(
109
- f"WARNING: A new version of devservices is available: {newest_version}"
110
- )
111
- console.warning('To update, run: "devservices update"')
112
-
113
104
 
114
105
  if __name__ == "__main__":
115
106
  main()
@@ -12,6 +12,8 @@ from dataclasses import dataclass
12
12
  from typing import TextIO
13
13
  from typing import TypeGuard
14
14
 
15
+ from sentry_sdk import set_context
16
+
15
17
  from devservices.configs.service_config import Dependency
16
18
  from devservices.configs.service_config import load_service_config_from_file
17
19
  from devservices.configs.service_config import RemoteConfig
@@ -36,6 +38,17 @@ from devservices.utils.services import find_matching_service
36
38
  from devservices.utils.services import Service
37
39
  from devservices.utils.state import State
38
40
 
41
+ RELEVANT_GIT_CONFIG_KEYS = [
42
+ "init.defaultbranch",
43
+ "core.sparsecheckout",
44
+ "remote.origin.url",
45
+ "remote.origin.fetch",
46
+ "remote.origin.promisor",
47
+ "remote.origin.partialclonefilter",
48
+ "protocol.version",
49
+ "extensions.partialclone",
50
+ ]
51
+
39
52
 
40
53
  class DependencyGraph:
41
54
  def __init__(self) -> None:
@@ -149,6 +162,28 @@ class GitConfigManager:
149
162
  if self.sparse_pattern:
150
163
  self.sparse_checkout_manager.set_sparse_checkout(self.sparse_pattern)
151
164
 
165
+ def get_relevant_config(self) -> dict[str, str]:
166
+ """
167
+ Get the relevant git config entries (to avoid logging sensitive information)
168
+ """
169
+ git_config = (
170
+ subprocess.check_output(
171
+ ["git", "config", "--list"],
172
+ cwd=self.repo_dir,
173
+ stderr=subprocess.PIPE,
174
+ )
175
+ .decode()
176
+ .strip()
177
+ )
178
+ git_config_dict = dict()
179
+ for line in git_config.split("\n"):
180
+ if not line:
181
+ continue
182
+ key, value = line.split("=")
183
+ if key in RELEVANT_GIT_CONFIG_KEYS:
184
+ git_config_dict[key] = value
185
+ return git_config_dict
186
+
152
187
  def _set_config(self, key: str, value: str) -> None:
153
188
  """
154
189
  Set a git config option for the repo
@@ -411,12 +446,15 @@ def _update_dependency(
411
446
  repo_link=dependency.repo_link,
412
447
  branch=dependency.branch,
413
448
  ) from e
449
+
414
450
  try:
415
451
  _run_command(
416
452
  ["git", "fetch", "origin", dependency.branch, "--filter=blob:none"],
417
453
  cwd=dependency_repo_dir,
418
454
  )
419
455
  except subprocess.CalledProcessError as e:
456
+ # Try to set the git config context to help with debugging
457
+ _try_set_git_config_context(git_config_manager)
420
458
  raise DependencyError(
421
459
  repo_name=dependency.repo_name,
422
460
  repo_link=dependency.repo_link,
@@ -536,6 +574,17 @@ def _run_command(
536
574
  subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
537
575
 
538
576
 
577
+ def _try_set_git_config_context(
578
+ git_config_manager: GitConfigManager,
579
+ ) -> None:
580
+ try:
581
+ git_config = git_config_manager.get_relevant_config()
582
+ set_context("git_config", git_config)
583
+ except subprocess.CalledProcessError as e:
584
+ logger = logging.getLogger(LOGGER_NAME)
585
+ logger.exception(e)
586
+
587
+
539
588
  def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
540
589
  dependency_repo_dir = os.path.join(
541
590
  DEVSERVICES_DEPENDENCIES_CACHE_DIR,