devservices 1.0.5__tar.gz → 1.0.7__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 (61) hide show
  1. {devservices-1.0.5 → devservices-1.0.7}/PKG-INFO +1 -1
  2. {devservices-1.0.5 → devservices-1.0.7}/README.md +1 -1
  3. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/down.py +12 -7
  4. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/logs.py +1 -1
  5. devservices-1.0.7/devservices/commands/purge.py +94 -0
  6. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/status.py +1 -1
  7. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/up.py +33 -6
  8. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/update.py +1 -1
  9. {devservices-1.0.5 → devservices-1.0.7}/devservices/constants.py +13 -1
  10. {devservices-1.0.5 → devservices-1.0.7}/devservices/exceptions.py +19 -2
  11. {devservices-1.0.5 → devservices-1.0.7}/devservices/main.py +2 -2
  12. devservices-1.0.7/devservices/utils/check_for_update.py +59 -0
  13. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/dependencies.py +49 -0
  14. devservices-1.0.7/devservices/utils/docker.py +219 -0
  15. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/docker_compose.py +44 -5
  16. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/PKG-INFO +1 -1
  17. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/SOURCES.txt +2 -1
  18. {devservices-1.0.5 → devservices-1.0.7}/pyproject.toml +1 -1
  19. devservices-1.0.7/tests/commands/test_purge.py +517 -0
  20. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_up.py +275 -2
  21. devservices-1.0.7/tests/utils/test_check_for_update.py +170 -0
  22. {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_dependencies.py +48 -0
  23. devservices-1.0.7/tests/utils/test_docker.py +493 -0
  24. {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_docker_compose.py +165 -99
  25. devservices-1.0.5/devservices/commands/check_for_update.py +0 -14
  26. devservices-1.0.5/devservices/commands/purge.py +0 -81
  27. devservices-1.0.5/devservices/utils/docker.py +0 -36
  28. devservices-1.0.5/tests/commands/test_purge.py +0 -167
  29. devservices-1.0.5/tests/utils/test_docker.py +0 -47
  30. {devservices-1.0.5 → devservices-1.0.7}/LICENSE.md +0 -0
  31. {devservices-1.0.5 → devservices-1.0.7}/devservices/__init__.py +0 -0
  32. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/__init__.py +0 -0
  33. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/list_dependencies.py +0 -0
  34. {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/list_services.py +0 -0
  35. {devservices-1.0.5 → devservices-1.0.7}/devservices/configs/service_config.py +0 -0
  36. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/__init__.py +0 -0
  37. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/console.py +0 -0
  38. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/devenv.py +0 -0
  39. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/file_lock.py +0 -0
  40. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/install_binary.py +0 -0
  41. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/services.py +0 -0
  42. {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/state.py +0 -0
  43. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/dependency_links.txt +0 -0
  44. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/entry_points.txt +0 -0
  45. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/requires.txt +0 -0
  46. {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/top_level.txt +0 -0
  47. {devservices-1.0.5 → devservices-1.0.7}/setup.cfg +0 -0
  48. {devservices-1.0.5 → devservices-1.0.7}/testing/__init__.py +0 -0
  49. {devservices-1.0.5 → devservices-1.0.7}/testing/utils.py +0 -0
  50. {devservices-1.0.5 → devservices-1.0.7}/tests/__init__.py +0 -0
  51. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_down.py +0 -0
  52. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_list_dependencies.py +0 -0
  53. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_list_services.py +0 -0
  54. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_logs.py +0 -0
  55. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_status.py +0 -0
  56. {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_update.py +0 -0
  57. {devservices-1.0.5 → devservices-1.0.7}/tests/configs/test_service_config.py +0 -0
  58. {devservices-1.0.5 → devservices-1.0.7}/tests/conftest.py +0 -0
  59. {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_install_binary.py +0 -0
  60. {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_services.py +0 -0
  61. {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -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.5
14
+ devservices==1.0.7
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
@@ -90,9 +90,14 @@ def down(args: Namespace) -> None:
90
90
  capture_exception(de)
91
91
  status.failure(str(de))
92
92
  exit(1)
93
- remote_dependencies = get_non_shared_remote_dependencies(
94
- service, remote_dependencies
95
- )
93
+ try:
94
+ remote_dependencies = get_non_shared_remote_dependencies(
95
+ service, remote_dependencies
96
+ )
97
+ except DependencyError as de:
98
+ capture_exception(de)
99
+ status.failure(str(de))
100
+ exit(1)
96
101
  try:
97
102
  _down(service, remote_dependencies, list(mode_dependencies), status)
98
103
  except DockerComposeError as dce:
@@ -106,12 +111,12 @@ def down(args: Namespace) -> None:
106
111
 
107
112
 
108
113
  def _bring_down_dependency(
109
- cmd: list[str], current_env: dict[str, str], status: Status
114
+ cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
110
115
  ) -> subprocess.CompletedProcess[str]:
111
116
  # TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
112
- for dependency in cmd[DOCKER_COMPOSE_COMMAND_LENGTH:]:
117
+ for dependency in cmd.services:
113
118
  status.info(f"Stopping {dependency}")
114
- return run_cmd(cmd, current_env)
119
+ return run_cmd(cmd.full_command, current_env)
115
120
 
116
121
 
117
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")
@@ -152,7 +152,7 @@ def _status(
152
152
 
153
153
  with concurrent.futures.ThreadPoolExecutor() as executor:
154
154
  futures = [
155
- executor.submit(run_cmd, cmd, current_env)
155
+ executor.submit(run_cmd, cmd.full_command, current_env)
156
156
  for cmd in docker_compose_commands
157
157
  ]
158
158
  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,12 @@ 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
110
  # 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]:
111
+ for dependency in cmd.services:
108
112
  status.info(f"Starting {dependency}")
109
- return run_cmd(cmd, current_env)
113
+ return run_cmd(cmd.full_command, current_env)
110
114
 
111
115
 
112
116
  def _up(
@@ -128,7 +132,7 @@ def _up(
128
132
  current_env[
129
133
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
130
134
  ] = relative_local_dependency_directory
131
- options = ["-d"]
135
+ options = ["-d", "--pull", "always"]
132
136
  dependency_graph = construct_dependency_graph(service, modes=modes)
133
137
  starting_order = dependency_graph.get_starting_order()
134
138
  sorted_remote_dependencies = sorted(
@@ -144,8 +148,31 @@ def _up(
144
148
  mode_dependencies=mode_dependencies,
145
149
  )
146
150
 
151
+ containers_to_check = []
152
+ with concurrent.futures.ThreadPoolExecutor() as dependency_executor:
153
+ futures = [
154
+ dependency_executor.submit(_bring_up_dependency, cmd, current_env, status)
155
+ for cmd in docker_compose_commands
156
+ ]
157
+ for future in concurrent.futures.as_completed(futures):
158
+ _ = future.result()
159
+
147
160
  for cmd in docker_compose_commands:
148
- _bring_up_dependency(cmd, current_env, status, len(options))
161
+ try:
162
+ container_names = get_container_names_for_project(
163
+ cmd.project_name, cmd.config_path
164
+ )
165
+ containers_to_check.extend(container_names)
166
+ except DockerComposeError as dce:
167
+ status.failure(
168
+ f"Failed to get containers to healthcheck for {cmd.project_name}: {dce.stderr}"
169
+ )
170
+ exit(1)
171
+ try:
172
+ check_all_containers_healthy(status, containers_to_check)
173
+ except ContainerHealthcheckFailedError as e:
174
+ status.failure(str(e))
175
+ exit(1)
149
176
 
150
177
 
151
178
  def _create_devservices_network() -> None:
@@ -7,10 +7,10 @@ from argparse import ArgumentParser
7
7
  from argparse import Namespace
8
8
  from importlib import metadata
9
9
 
10
- from devservices.commands.check_for_update import check_for_update
11
10
  from devservices.constants import DEVSERVICES_DOWNLOAD_URL
12
11
  from devservices.exceptions import BinaryInstallError
13
12
  from devservices.exceptions import DevservicesUpdateError
13
+ from devservices.utils.check_for_update import check_for_update
14
14
  from devservices.utils.console import Console
15
15
  from devservices.utils.install_binary import install_binary
16
16
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from datetime import timedelta
4
5
 
5
6
  MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
6
7
  DEVSERVICES_DIR_NAME = "devservices"
@@ -12,7 +13,7 @@ DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
12
13
  DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
13
14
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
14
15
  STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
15
- DOCKER_COMPOSE_COMMAND_LENGTH = 7
16
+ DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
16
17
 
17
18
  DEPENDENCY_CONFIG_VERSION = "v1"
18
19
  DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
@@ -21,9 +22,20 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
21
22
  "core.sparseCheckout": "true",
22
23
  }
23
24
 
25
+ DEVSERVICES_RELEASES_URL = (
26
+ "https://api.github.com/repos/getsentry/devservices/releases/latest"
27
+ )
24
28
  DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
25
29
  DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
26
30
  BINARY_PERMISSIONS = 0o755
27
31
  MAX_LOG_LINES = "100"
28
32
  LOGGER_NAME = "devservices"
29
33
  DOCKER_NETWORK_NAME = "devservices"
34
+
35
+ # Latest Version Cache
36
+ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
37
+ DEVSERVICES_CACHE_DIR, "latest_version.txt"
38
+ )
39
+ DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
40
+ HEALTHCHECK_TIMEOUT = 30
41
+ HEALTHCHECK_INTERVAL = 5
@@ -57,8 +57,8 @@ class DockerComposeInstallationError(BinaryInstallError):
57
57
  pass
58
58
 
59
59
 
60
- class DockerComposeError(Exception):
61
- """Base class for Docker Compose related errors."""
60
+ class DockerError(Exception):
61
+ """Base class for Docker related errors."""
62
62
 
63
63
  def __init__(self, command: str, returncode: int, stdout: str, stderr: str):
64
64
  self.command = command
@@ -67,6 +67,12 @@ class DockerComposeError(Exception):
67
67
  self.stderr = stderr
68
68
 
69
69
 
70
+ class DockerComposeError(DockerError):
71
+ """Base class for Docker Compose related errors."""
72
+
73
+ pass
74
+
75
+
70
76
  class ModeDoesNotExistError(Exception):
71
77
  """Raised when a mode does not exist."""
72
78
 
@@ -121,3 +127,14 @@ class FailedToSetGitConfigError(GitConfigError):
121
127
  """Raised when a git config cannot be set."""
122
128
 
123
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."
@@ -23,10 +23,10 @@ from devservices.commands import purge
23
23
  from devservices.commands import status
24
24
  from devservices.commands import up
25
25
  from devservices.commands import update
26
- from devservices.commands.check_for_update import check_for_update
27
26
  from devservices.constants import LOGGER_NAME
28
27
  from devservices.exceptions import DockerComposeInstallationError
29
28
  from devservices.exceptions import DockerDaemonNotRunningError
29
+ from devservices.utils.check_for_update import check_for_update
30
30
  from devservices.utils.console import Console
31
31
  from devservices.utils.docker_compose import check_docker_compose_version
32
32
 
@@ -102,7 +102,7 @@ def main() -> None:
102
102
  else:
103
103
  parser.print_help()
104
104
 
105
- if args.command != "update":
105
+ if args.command != "update" and os.environ.get("CI") != "true":
106
106
  newest_version = check_for_update()
107
107
  if newest_version != current_version:
108
108
  console.warning(
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+ from datetime import timedelta
7
+ from urllib.request import urlopen
8
+
9
+ from devservices.constants import DEVSERVICES_CACHE_DIR
10
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_FILE
11
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
12
+ from devservices.constants import DEVSERVICES_RELEASES_URL
13
+
14
+
15
+ def _delete_cached_version() -> None:
16
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
17
+ os.remove(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
18
+
19
+
20
+ def _get_cache_age() -> timedelta:
21
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
22
+ file_modification_time = datetime.fromtimestamp(
23
+ os.path.getmtime(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
24
+ )
25
+ return datetime.now() - file_modification_time
26
+ return timedelta.max
27
+
28
+
29
+ def _get_cached_version() -> str | None:
30
+ cache_age = _get_cache_age()
31
+ if cache_age < DEVSERVICES_LATEST_VERSION_CACHE_TTL:
32
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "r", encoding="utf-8") as f:
33
+ return f.read()
34
+ else:
35
+ _delete_cached_version()
36
+ return None
37
+
38
+
39
+ def _set_cached_version(latest_version: str) -> None:
40
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "w", encoding="utf-8") as f:
41
+ f.write(latest_version)
42
+
43
+
44
+ def check_for_update() -> str | None:
45
+ os.makedirs(DEVSERVICES_CACHE_DIR, exist_ok=True)
46
+
47
+ cached_version = _get_cached_version()
48
+ if cached_version is not None:
49
+ return cached_version
50
+
51
+ with urlopen(DEVSERVICES_RELEASES_URL) as response:
52
+ if response.status == 200:
53
+ data = json.loads(response.read())
54
+ latest_version = str(data["tag_name"])
55
+
56
+ _set_cached_version(latest_version)
57
+
58
+ return latest_version
59
+ return None
@@ -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,