doover-cli 0.6.1__tar.gz → 0.6.3__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 (91) hide show
  1. {doover_cli-0.6.1 → doover_cli-0.6.3}/PKG-INFO +2 -2
  2. {doover_cli-0.6.1 → doover_cli-0.6.3}/pyproject.toml +2 -1
  3. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/__init__.py +1 -1
  4. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/apps/apps.py +179 -10
  5. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/config_schema.py +6 -3
  6. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/login.py +18 -0
  7. doover_cli-0.6.3/src/doover_cli/registry.py +373 -0
  8. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/ui_schema.py +6 -3
  9. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/apps.py +173 -0
  10. doover_cli-0.6.3/tests/test_discover.py +157 -0
  11. doover_cli-0.6.3/tests/test_registry.py +432 -0
  12. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_utils_apps.py +86 -0
  13. {doover_cli-0.6.1 → doover_cli-0.6.3}/uv.lock +609 -610
  14. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/CONTRIBUTING.md +0 -0
  15. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  16. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  18. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/workflows/lint-test.yml +0 -0
  20. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/workflows/release-brew.yml +0 -0
  21. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/workflows/release-pypi.yml +0 -0
  22. {doover_cli-0.6.1 → doover_cli-0.6.3}/.github/workflows/release.yml +0 -0
  23. {doover_cli-0.6.1 → doover_cli-0.6.3}/.gitignore +0 -0
  24. {doover_cli-0.6.1 → doover_cli-0.6.3}/.pre-commit-config.yaml +0 -0
  25. {doover_cli-0.6.1 → doover_cli-0.6.3}/AGENT.md +0 -0
  26. {doover_cli-0.6.1 → doover_cli-0.6.3}/LICENSE +0 -0
  27. {doover_cli-0.6.1 → doover_cli-0.6.3}/README.md +0 -0
  28. {doover_cli-0.6.1 → doover_cli-0.6.3}/debian/changelog +0 -0
  29. {doover_cli-0.6.1 → doover_cli-0.6.3}/debian/control +0 -0
  30. {doover_cli-0.6.1 → doover_cli-0.6.3}/debian/rules +0 -0
  31. {doover_cli-0.6.1 → doover_cli-0.6.3}/docs/user_stories.md +0 -0
  32. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/agent.py +0 -0
  33. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/api/__init__.py +0 -0
  34. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/api/auth.py +0 -0
  35. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/api/errors.py +0 -0
  36. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/api/session.py +0 -0
  37. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/apps/app_install.py +0 -0
  38. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/apps/device.py +0 -0
  39. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/apps/device_type.py +0 -0
  40. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/apps/tunnel.py +0 -0
  41. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/channel.py +0 -0
  42. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/colours.py +0 -0
  43. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/dda_logs.py +0 -0
  44. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/doover_config.py +0 -0
  45. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/grpc.py +0 -0
  46. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/renderer/__init__.py +0 -0
  47. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/renderer/_base.py +0 -0
  48. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/renderer/_basic.py +0 -0
  49. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/renderer/_default.py +0 -0
  50. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/renderer/_json.py +0 -0
  51. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/report.py +0 -0
  52. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/simulator.py +0 -0
  53. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/user.py +0 -0
  54. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/__init__.py +0 -0
  55. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/api.py +0 -0
  56. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/context.py +0 -0
  57. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/__init__.py +0 -0
  58. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/commands.py +0 -0
  59. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/lookup.py +0 -0
  60. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/prompting.py +0 -0
  61. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/schema.py +0 -0
  62. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/crud/values.py +0 -0
  63. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/errors.py +0 -0
  64. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/formatters.py +0 -0
  65. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/misc.py +0 -0
  66. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/parsers.py +0 -0
  67. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/prompt.py +0 -0
  68. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/sentry.py +0 -0
  69. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/shell_commands.py +0 -0
  70. {doover_cli-0.6.1 → doover_cli-0.6.3}/src/doover_cli/utils/state.py +0 -0
  71. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/__init__.py +0 -0
  72. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/conftest.py +0 -0
  73. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_agent.py +0 -0
  74. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_app_install.py +0 -0
  75. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_apps.py +0 -0
  76. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_auth_integration.py +0 -0
  77. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_auth_unit.py +0 -0
  78. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_basic.py +0 -0
  79. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_crud_commands.py +0 -0
  80. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_crud_lookup.py +0 -0
  81. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_crud_prompting.py +0 -0
  82. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_crud_schema.py +0 -0
  83. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_crud_values.py +0 -0
  84. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_default_renderer.py +0 -0
  85. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_device.py +0 -0
  86. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_device_cli_integration.py +0 -0
  87. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_device_type.py +0 -0
  88. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_device_type_cli_integration.py +0 -0
  89. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_prompt.py +0 -0
  90. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_sentry.py +0 -0
  91. {doover_cli-0.6.1 → doover_cli-0.6.3}/tests/test_user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doover-cli
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: CLI for Doover
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -10,7 +10,7 @@ Requires-Dist: docker>=7.1.0
10
10
  Requires-Dist: jsf>=0.11.2
11
11
  Requires-Dist: paramiko>=3.5.1
12
12
  Requires-Dist: prompt-toolkit>=3.0.52
13
- Requires-Dist: pydoover>=1.5.1
13
+ Requires-Dist: pydoover>=1.12.0
14
14
  Requires-Dist: pytz>=2025.2
15
15
  Requires-Dist: questionary>=1.10.0
16
16
  Requires-Dist: requests>=2.32.3
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "docker>=7.1.0",
11
11
  "jsf>=0.11.2",
12
12
  "paramiko>=3.5.1",
13
- "pydoover>=1.5.1",
13
+ "pydoover>=1.12.0",
14
14
  "pytz>=2025.2",
15
15
  "questionary>=1.10.0",
16
16
  "requests>=2.32.3",
@@ -31,6 +31,7 @@ path = "src/doover_cli/__init__.py"
31
31
 
32
32
  [project.scripts]
33
33
  doover = "doover_cli:main"
34
+ docker-credential-doover = "doover_cli.registry:credential_helper"
34
35
 
35
36
  [dependency-groups]
36
37
  dev = [
@@ -1,7 +1,7 @@
1
1
  from doover_cli.renderer import Renderer
2
2
  from typing import Annotated, Optional
3
3
 
4
- __version__ = "0.6.1"
4
+ __version__ = "0.6.3"
5
5
 
6
6
  import click
7
7
  import typer
@@ -7,6 +7,8 @@ import re
7
7
  import shutil
8
8
  import socket
9
9
  import subprocess
10
+ import sys
11
+ import tempfile
10
12
  import time
11
13
  from urllib.parse import urlencode
12
14
  from pathlib import Path
@@ -26,7 +28,10 @@ import questionary
26
28
  from ..config_schema import export as export_config_command
27
29
  from ..ui_schema import export as export_ui_command
28
30
  from ..utils.api import ProfileAnnotation
31
+ from ..registry import is_doover_registry, login_for_push, publish_github_output
29
32
  from ..utils.apps import (
33
+ PACKAGE_APP_TYPES,
34
+ discover_apps,
30
35
  get_app_directory,
31
36
  call_with_uv,
32
37
  get_docker_path,
@@ -120,6 +125,20 @@ def _detect_git_commit(root_fp: Path) -> str:
120
125
  return result.stdout.strip() if result.returncode == 0 else ""
121
126
 
122
127
 
128
+ def _is_multi_platform(build_args: str) -> bool:
129
+ """Whether `build_args` asks for more than one platform.
130
+
131
+ Multi-platform changes how the image has to be built: buildx cannot load a
132
+ multi-platform result into the local daemon, so it must push directly, and
133
+ the digest has to come from buildx rather than the daemon.
134
+ """
135
+ for token in ("--platform", "--platform="):
136
+ if token in build_args:
137
+ tail = build_args.split(token, 1)[1].lstrip("= ").split()[0]
138
+ return "," in tail
139
+ return False
140
+
141
+
123
142
  def _build_container(
124
143
  root_fp: Path, *, buildx: bool, build_args: str, image_name: str
125
144
  ) -> None:
@@ -128,6 +147,29 @@ def _build_container(
128
147
  )
129
148
 
130
149
 
150
+ def _build_and_push_multi_platform(
151
+ root_fp: Path, *, build_args: str, image_name: str
152
+ ) -> str | None:
153
+ """Build and push a multi-platform image in one step, returning its digest.
154
+
155
+ A multi-platform build cannot be loaded into the local daemon, so `build`
156
+ then `push` does not work -- and `docker inspect` afterwards reports either
157
+ nothing or the single-platform digest, not the index. buildx writes the real
158
+ digest to a metadata file, which is the only reliable source.
159
+ """
160
+ with tempfile.TemporaryDirectory() as tmp:
161
+ metadata_fp = Path(tmp) / "metadata.json"
162
+ shell_run(
163
+ f"docker buildx build {build_args} --push "
164
+ f"--metadata-file {metadata_fp} -t {image_name} {str(root_fp)}",
165
+ )
166
+ try:
167
+ metadata = json.loads(metadata_fp.read_text())
168
+ except (OSError, ValueError):
169
+ return None
170
+ return metadata.get("containerimage.digest")
171
+
172
+
131
173
  def _push_container(image_name: str) -> None:
132
174
  shell_run(f"docker push {image_name}")
133
175
 
@@ -925,7 +967,13 @@ def publish(
925
967
  ] = Path(),
926
968
  build_container: Annotated[
927
969
  bool,
928
- typer.Option(help="Build and push the container image to the registry."),
970
+ typer.Option(
971
+ # `--build` is the name this is known by; the longer form is kept so
972
+ # existing scripts and CI keep working.
973
+ "--build/--no-build",
974
+ "--build-container/--no-build-container",
975
+ help="Build and push the container image to the registry.",
976
+ ),
929
977
  ] = False,
930
978
  staging: Annotated[
931
979
  bool | None,
@@ -1123,7 +1171,7 @@ def publish(
1123
1171
  )
1124
1172
  rich.print("[green]Widget uploaded.[/green]")
1125
1173
 
1126
- if app_config.type in ("PRO", "REP", "INT"):
1174
+ if app_config.type in PACKAGE_APP_TYPES:
1127
1175
  if build_package:
1128
1176
  print("\nBuilding package.zip for upload...")
1129
1177
  shell_run("./build.sh", cwd=root_fp)
@@ -1164,14 +1212,49 @@ def publish(
1164
1212
  build_args = getattr(app_config, "build_args", "") or ""
1165
1213
  if build_args != "NO_BUILD":
1166
1214
  print("\nBuilding and pushing container image to the registry...")
1167
- _build_container(
1168
- root_fp,
1169
- buildx=buildx,
1170
- build_args=build_args,
1171
- image_name=image_name,
1172
- )
1173
- _push_container(image_name)
1174
- detected_digest = _get_image_digest(image_name)
1215
+
1216
+ # Pushing to the doover registry needs a credential scoped to this
1217
+ # app's repository, minted against the publish permission we just
1218
+ # exercised. Other registries keep using whatever docker login the
1219
+ # user already has.
1220
+ if is_doover_registry(image_name, _control_base_url()):
1221
+ # `response` is an Application model on create/partial; fall back to
1222
+ # resolving by name for the paths that don't return one.
1223
+ app_id = getattr(response, "id", None)
1224
+ if app_id is None and isinstance(response, dict):
1225
+ app_id = response.get("id")
1226
+ if app_id is None:
1227
+ app_id = _resolve_application_id(
1228
+ client, app_config, staging=resolved_staging
1229
+ )
1230
+ if not app_id:
1231
+ raise typer.BadParameter(
1232
+ "Could not determine the application id to mint a registry "
1233
+ "credential. Publish the app first."
1234
+ )
1235
+ login_for_push(client, app_id)
1236
+
1237
+ if _is_multi_platform(build_args):
1238
+ # buildx pushes as part of the build here; see the helper for why
1239
+ # build-then-push cannot work for multi-platform.
1240
+ detected_digest = _build_and_push_multi_platform(
1241
+ root_fp, build_args=build_args, image_name=image_name
1242
+ )
1243
+ else:
1244
+ _build_container(
1245
+ root_fp,
1246
+ buildx=buildx,
1247
+ build_args=build_args,
1248
+ image_name=image_name,
1249
+ )
1250
+ _push_container(image_name)
1251
+ detected_digest = _get_image_digest(image_name)
1252
+
1253
+ if detected_digest is None:
1254
+ print(
1255
+ "Warning: could not determine the pushed image digest. "
1256
+ "Pass --digest to release against it explicitly."
1257
+ )
1175
1258
  else:
1176
1259
  print("App requested to not build. Skipping build step.")
1177
1260
 
@@ -1193,6 +1276,92 @@ def publish(
1193
1276
  renderer.render(response)
1194
1277
 
1195
1278
 
1279
+ @app.command()
1280
+ def discover(
1281
+ root: Annotated[Path, typer.Argument(help="Repository root to search.")] = Path(),
1282
+ as_json: Annotated[
1283
+ bool,
1284
+ typer.Option(
1285
+ "--json",
1286
+ help="Emit a single-line JSON array, suitable for a CI matrix.",
1287
+ ),
1288
+ ] = False,
1289
+ ):
1290
+ """List every application in this repository and how to build it.
1291
+
1292
+ Handles both shapes a repo can take: several app entries in one
1293
+ doover_config.json, and several self-contained app directories each with their
1294
+ own. CI reads this to build its matrix instead of the workflow hard-coding app
1295
+ names, which is what lets one workflow file serve every app repo.
1296
+ """
1297
+ found = discover_apps(root)
1298
+ if not found:
1299
+ print(
1300
+ f"No applications found under {root}. Each app needs a "
1301
+ f"doover_config.json with a `type` set.",
1302
+ file=sys.stderr,
1303
+ )
1304
+ raise typer.Exit(1)
1305
+
1306
+ for entry in found:
1307
+ entry["dir"] = os.path.normpath(os.path.join(root, entry["dir"]))
1308
+
1309
+ if as_json:
1310
+ # Compact and on one line so it can be captured straight into a step output.
1311
+ print(json.dumps(found, separators=(",", ":")))
1312
+ raise typer.Exit(0)
1313
+
1314
+ for entry in found:
1315
+ bits = [entry["type"] or "?", entry["language"] or "unknown language"]
1316
+ if entry["widget"]:
1317
+ bits.append("widget")
1318
+ print(f"{entry['name']:<40} {entry['dir']:<24} {', '.join(bits)}")
1319
+
1320
+
1321
+ @app.command(name="registry-login")
1322
+ def registry_login(
1323
+ app_fp: Annotated[
1324
+ Path, typer.Argument(help="Path to the application directory.")
1325
+ ] = Path(),
1326
+ app_name: Annotated[
1327
+ str | None,
1328
+ typer.Option(help="Which app in doover_config.json, if it defines several."),
1329
+ ] = None,
1330
+ staging: Annotated[
1331
+ bool | None,
1332
+ typer.Option(help="Force staging mode. Defaults to matching the API URL."),
1333
+ ] = None,
1334
+ ):
1335
+ """`docker login` to the Doover registry for this application.
1336
+
1337
+ For CI, which builds with buildx rather than `publish --build` and so needs
1338
+ the credential in place beforehand. The token is scoped to this application's
1339
+ repository only, and is piped straight into docker -- it is never printed, so
1340
+ it cannot end up in a workflow log or a step output.
1341
+
1342
+ Publish the app first: the credential is minted against its publish
1343
+ permission, and the repository comes from its registered image name.
1344
+ """
1345
+ client, _ = get_state()
1346
+ root_fp = get_app_directory(app_fp)
1347
+ app_config = get_app_config(root_fp, app_name=app_name)
1348
+
1349
+ app_id = _resolve_application_id(
1350
+ client, app_config, staging=_resolve_staging(staging)
1351
+ )
1352
+ if app_id is None:
1353
+ raise typer.BadParameter(
1354
+ f"'{app_config.name}' does not exist yet. Run `doover app publish` "
1355
+ f"before logging in to the registry."
1356
+ )
1357
+
1358
+ target = login_for_push(client, app_id)
1359
+ # Emitted so the build step can tag from the app's registered image name
1360
+ # instead of the workflow repeating it.
1361
+ publish_github_output(target)
1362
+ print(f"Logged in to push {target}")
1363
+
1364
+
1196
1365
  @app.command(name="release")
1197
1366
  def release_command(
1198
1367
  app_fp: Annotated[
@@ -8,6 +8,7 @@ import typer
8
8
  import jsf
9
9
 
10
10
  from .utils.apps import (
11
+ app_names_in_config,
11
12
  get_app_directory,
12
13
  call_with_uv,
13
14
  get_app_config,
@@ -94,10 +95,12 @@ def validate(
94
95
  config_file = root_fp / "doover_config.json"
95
96
 
96
97
  if export_ is True:
97
- # Validate the schema the Python *currently* generates, without leaving
98
- # doover_config.json modified (e.g. when run as a pre-commit hook).
98
+ targets = [app_name] if app_name else app_names_in_config(root_fp) or [None]
99
99
  with preserve_file(config_file):
100
- ctx.invoke(export, ctx, app_fp=root_fp, validate_=False, app_name=app_name)
100
+ for target in targets:
101
+ ctx.invoke(
102
+ export, ctx, app_fp=root_fp, validate_=False, app_name=target
103
+ )
101
104
  _validate_config_file(config_file)
102
105
  else:
103
106
  _validate_config_file(config_file)
@@ -5,6 +5,8 @@ from typer import Typer
5
5
 
6
6
  from doover_cli.api import DooverCLIAuthClient
7
7
 
8
+ from .registry import register_credential_helper, registry_host
9
+
8
10
  from .utils.sentry import capture_handled_exception
9
11
  from .utils.state import state
10
12
 
@@ -47,3 +49,19 @@ def login(
47
49
  print(
48
50
  f"Successfully logged into Doover ({environment}). You can now run `doover ... --profile {profile_name}`."
49
51
  )
52
+
53
+ # Point docker at our credential helper so `docker pull`/`push` against the
54
+ # doover registry just work, with no `docker login` and no credentials
55
+ # written to disk. Best-effort: failing to edit the user's docker config is
56
+ # not a reason to fail the login.
57
+ try:
58
+ # Register the registry that belongs to the environment just logged into,
59
+ # so a staging login makes staging images pullable rather than production's.
60
+ if register_credential_helper(registry_host(auth.control_base_url)):
61
+ print(
62
+ "Configured docker to use your Doover login for "
63
+ f"{registry_host(auth.control_base_url)} images."
64
+ )
65
+ except Exception:
66
+ if state.debug:
67
+ raise