doover-cli 0.3.3__tar.gz → 0.4.2__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 (89) hide show
  1. {doover_cli-0.3.3 → doover_cli-0.4.2}/PKG-INFO +1 -1
  2. {doover_cli-0.3.3 → doover_cli-0.4.2}/pyproject.toml +1 -1
  3. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/api/session.py +27 -0
  4. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/apps/apps.py +89 -8
  5. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/api.py +24 -0
  6. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/apps.py +14 -10
  7. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_apps.py +8 -9
  8. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_auth_unit.py +46 -0
  9. doover_cli-0.4.2/uv.lock +2034 -0
  10. doover_cli-0.3.3/uv.lock +0 -2000
  11. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/CONTRIBUTING.md +0 -0
  12. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  13. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  14. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  15. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/workflows/lint-test.yml +0 -0
  17. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/workflows/release-brew.yml +0 -0
  18. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/workflows/release-pypi.yml +0 -0
  19. {doover_cli-0.3.3 → doover_cli-0.4.2}/.github/workflows/release.yml +0 -0
  20. {doover_cli-0.3.3 → doover_cli-0.4.2}/.gitignore +0 -0
  21. {doover_cli-0.3.3 → doover_cli-0.4.2}/.pre-commit-config.yaml +0 -0
  22. {doover_cli-0.3.3 → doover_cli-0.4.2}/AGENT.md +0 -0
  23. {doover_cli-0.3.3 → doover_cli-0.4.2}/LICENSE +0 -0
  24. {doover_cli-0.3.3 → doover_cli-0.4.2}/README.md +0 -0
  25. {doover_cli-0.3.3 → doover_cli-0.4.2}/debian/changelog +0 -0
  26. {doover_cli-0.3.3 → doover_cli-0.4.2}/debian/control +0 -0
  27. {doover_cli-0.3.3 → doover_cli-0.4.2}/debian/rules +0 -0
  28. {doover_cli-0.3.3 → doover_cli-0.4.2}/docs/user_stories.md +0 -0
  29. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/__init__.py +0 -0
  30. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/agent.py +0 -0
  31. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/api/__init__.py +0 -0
  32. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/api/auth.py +0 -0
  33. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/api/errors.py +0 -0
  34. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/apps/app_install.py +0 -0
  35. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/apps/device.py +0 -0
  36. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/apps/device_type.py +0 -0
  37. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/apps/tunnel.py +0 -0
  38. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/channel.py +0 -0
  39. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/colours.py +0 -0
  40. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/config_schema.py +0 -0
  41. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/dda_logs.py +0 -0
  42. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/doover_config.py +0 -0
  43. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/grpc.py +0 -0
  44. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/login.py +0 -0
  45. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/renderer/__init__.py +0 -0
  46. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/renderer/_base.py +0 -0
  47. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/renderer/_basic.py +0 -0
  48. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/renderer/_default.py +0 -0
  49. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/renderer/_json.py +0 -0
  50. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/report.py +0 -0
  51. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/simulator.py +0 -0
  52. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/ui_schema.py +0 -0
  53. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/user.py +0 -0
  54. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/__init__.py +0 -0
  55. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/context.py +0 -0
  56. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/__init__.py +0 -0
  57. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/commands.py +0 -0
  58. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/lookup.py +0 -0
  59. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/prompting.py +0 -0
  60. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/schema.py +0 -0
  61. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/crud/values.py +0 -0
  62. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/errors.py +0 -0
  63. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/formatters.py +0 -0
  64. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/misc.py +0 -0
  65. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/parsers.py +0 -0
  66. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/prompt.py +0 -0
  67. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/sentry.py +0 -0
  68. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/shell_commands.py +0 -0
  69. {doover_cli-0.3.3 → doover_cli-0.4.2}/src/doover_cli/utils/state.py +0 -0
  70. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/__init__.py +0 -0
  71. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/conftest.py +0 -0
  72. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_agent.py +0 -0
  73. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_app_install.py +0 -0
  74. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_auth_integration.py +0 -0
  75. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_basic.py +0 -0
  76. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_crud_commands.py +0 -0
  77. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_crud_lookup.py +0 -0
  78. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_crud_prompting.py +0 -0
  79. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_crud_schema.py +0 -0
  80. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_crud_values.py +0 -0
  81. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_default_renderer.py +0 -0
  82. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_device.py +0 -0
  83. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_device_cli_integration.py +0 -0
  84. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_device_type.py +0 -0
  85. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_device_type_cli_integration.py +0 -0
  86. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_prompt.py +0 -0
  87. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_sentry.py +0 -0
  88. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_user.py +0 -0
  89. {doover_cli-0.3.3 → doover_cli-0.4.2}/tests/test_utils_apps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doover-cli
3
- Version: 0.3.3
3
+ Version: 0.4.2
4
4
  Summary: CLI for Doover
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "doover-cli"
3
- version = "0.3.3"
3
+ version = "0.4.2"
4
4
  description = "CLI for Doover"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
@@ -45,6 +45,33 @@ class DooverCLISession:
45
45
  auth=auth,
46
46
  )
47
47
 
48
+ @classmethod
49
+ def from_trusted_publisher(
50
+ cls,
51
+ *,
52
+ provider: str = "GH",
53
+ oidc_token: str | None = None,
54
+ audience: str | None = None,
55
+ control_base_url: str | None = None,
56
+ timeout: float = 60.0,
57
+ ) -> "DooverCLISession":
58
+ """Build a session that publishes via the trusted-publisher OIDC flow —
59
+ no stored Doover secret. Inside GitHub Actions (provider 'GH' with
60
+ id-token permission) the OIDC token is fetched automatically."""
61
+ from pydoover.api.auth import TrustedPublisherAuthClient
62
+
63
+ # audience defaults to pydoover's DEFAULT_DOOVER_OIDC_AUDIENCE when unset.
64
+ kwargs = dict(
65
+ provider=provider,
66
+ oidc_token=oidc_token,
67
+ control_base_url=control_base_url,
68
+ timeout=timeout,
69
+ )
70
+ if audience:
71
+ kwargs["audience"] = audience
72
+ auth = TrustedPublisherAuthClient(**kwargs)
73
+ return cls(config_manager=None, profile_name=None, auth=auth)
74
+
48
75
  @classmethod
49
76
  def from_env(cls, *, timeout: float = 60.0) -> "DooverCLISession":
50
77
  token = os.environ.get("DOOVER_API_TOKEN")
@@ -104,6 +104,21 @@ def _resolve_staging(staging: bool | None) -> bool:
104
104
  return ".staging." in _control_base_url()
105
105
 
106
106
 
107
+ def _detect_git_commit(root_fp: Path) -> str:
108
+ """Best-effort current git commit SHA for the app dir (empty if unavailable)."""
109
+ try:
110
+ result = subprocess.run(
111
+ ["git", "rev-parse", "HEAD"],
112
+ cwd=root_fp,
113
+ capture_output=True,
114
+ text=True,
115
+ check=False,
116
+ )
117
+ except OSError:
118
+ return ""
119
+ return result.stdout.strip() if result.returncode == 0 else ""
120
+
121
+
107
122
  def _build_container(
108
123
  root_fp: Path, *, buildx: bool, build_args: str, image_name: str
109
124
  ) -> None:
@@ -1049,15 +1064,10 @@ def publish(
1049
1064
  application_id,
1050
1065
  root_fp,
1051
1066
  )
1052
- print("Done!")
1053
-
1054
- print("Creating new lambda version release...")
1055
- version_response = client.applications.processor_version(
1056
- str(application_id),
1057
- body=dict(payload),
1067
+ print(
1068
+ "Done! Run `doover app release` to publish an immutable version of this app."
1058
1069
  )
1059
- print("Done!")
1060
- renderer.render(version_response or processor_response or response)
1070
+ renderer.render(processor_response or response)
1061
1071
  raise typer.Exit(0)
1062
1072
 
1063
1073
  if build_container:
@@ -1079,6 +1089,77 @@ def publish(
1079
1089
  renderer.render(response)
1080
1090
 
1081
1091
 
1092
+ @app.command(name="release")
1093
+ def release(
1094
+ app_fp: Annotated[
1095
+ Path, typer.Argument(help="Path to the application directory.")
1096
+ ] = Path(),
1097
+ digest: Annotated[
1098
+ str | None,
1099
+ typer.Option(
1100
+ help="Pushed image digest (e.g. sha256:…) for container apps. Ignored for processors."
1101
+ ),
1102
+ ] = None,
1103
+ tag: Annotated[
1104
+ str | None,
1105
+ typer.Option(help="Optional version label (e.g. semver / image tag)."),
1106
+ ] = None,
1107
+ commit: Annotated[
1108
+ str | None,
1109
+ typer.Option(
1110
+ help="Source git commit SHA (for browsing the code). Defaults to the repo's HEAD."
1111
+ ),
1112
+ ] = None,
1113
+ notes: Annotated[str, typer.Option(help="Optional release notes.")] = "",
1114
+ staging: Annotated[
1115
+ bool | None,
1116
+ typer.Option(help="Force staging mode (default: inferred from the API URL)."),
1117
+ ] = None,
1118
+ app_name: Annotated[
1119
+ str | None,
1120
+ typer.Option(help="Application name in doover_config.json."),
1121
+ ] = None,
1122
+ _profile: ProfileAnnotation = None,
1123
+ ):
1124
+ """Publish an immutable version (release) of an application.
1125
+
1126
+ Run `doover app publish` first (it updates the app's metadata + config/UI
1127
+ schema from doover_config.json). For container apps pass the pushed image
1128
+ --digest; for processors the lambda version is published server-side.
1129
+ """
1130
+ _ = _profile
1131
+ root_fp = get_app_directory(app_fp)
1132
+ app_config = get_app_config(root_fp, app_name=app_name)
1133
+ resolved_staging = _resolve_staging(staging)
1134
+
1135
+ application_id = _get_persisted_application_id(app_config, staging=resolved_staging)
1136
+ if application_id is None:
1137
+ rich.print(
1138
+ "[red]Application has not been published yet. Run `doover app publish` first.[/red]"
1139
+ )
1140
+ raise typer.Exit(1)
1141
+
1142
+ if app_config.type == "DEV" and not digest:
1143
+ rich.print(
1144
+ "[red]--digest is required to release a container application.[/red]"
1145
+ )
1146
+ raise typer.Exit(1)
1147
+
1148
+ resolved_commit = commit if commit is not None else _detect_git_commit(root_fp)
1149
+
1150
+ client, renderer = get_state()
1151
+ with renderer.loading("Creating application version..."):
1152
+ version = client.create_application_version(
1153
+ application_id,
1154
+ digest=digest,
1155
+ tag=tag or "",
1156
+ commit=resolved_commit or "",
1157
+ notes=notes or "",
1158
+ )
1159
+ rich.print("[green]Released a new application version.[/green]")
1160
+ renderer.render(version)
1161
+
1162
+
1082
1163
  @app.command(
1083
1164
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
1084
1165
  )
@@ -8,6 +8,22 @@ from typer import Option
8
8
  from doover_cli.api import ControlClientUnavailableError, DooverCLISession
9
9
 
10
10
 
11
+ def _trusted_publisher_provider() -> str | None:
12
+ """The CI provider to use for the trusted-publisher (OIDC) flow, or None.
13
+
14
+ Triggers on explicit opt-in (`DOOVER_TRUSTED_PUBLISHER`, value is the provider
15
+ or a truthy flag meaning GitHub), or automatically inside a GitHub Actions job
16
+ that has `permissions: id-token: write` — so `doover app publish` just works."""
17
+ explicit = os.environ.get("DOOVER_TRUSTED_PUBLISHER")
18
+ if explicit:
19
+ return "GH" if explicit.lower() in ("1", "true", "yes", "github") else explicit
20
+ if os.environ.get("GITHUB_ACTIONS") == "true" and os.environ.get(
21
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
22
+ ):
23
+ return "GH"
24
+ return None
25
+
26
+
11
27
  def setup_session(
12
28
  profile_name: str,
13
29
  config_manager: ConfigManager | None = None,
@@ -15,6 +31,14 @@ def setup_session(
15
31
  if os.environ.get("DOOVER_API_TOKEN"):
16
32
  return DooverCLISession.from_env()
17
33
 
34
+ provider = _trusted_publisher_provider()
35
+ if provider:
36
+ return DooverCLISession.from_trusted_publisher(
37
+ provider=provider,
38
+ audience=os.environ.get("DOOVER_OIDC_AUDIENCE"),
39
+ control_base_url=os.environ.get("DOOVER_CONTROL_API_BASE_URL"),
40
+ )
41
+
18
42
  manager = config_manager or ConfigManager(profile_name)
19
43
  manager.current_profile = profile_name
20
44
  return DooverCLISession.from_profile(profile_name, config_manager=manager)
@@ -306,16 +306,20 @@ def get_app_directory(root: Path | None = None) -> Path:
306
306
 
307
307
 
308
308
  def get_uv_path() -> Path:
309
- brew = Path("/opt/homebrew/bin/uv")
310
- if brew.exists():
311
- return brew
312
-
313
- uv_path = Path.home() / ".local" / "bin" / "uv"
314
- if not uv_path.exists():
315
- raise RuntimeError(
316
- "uv not found in your PATH. Please install it and try again."
317
- )
318
- return uv_path
309
+ # Prefer uv on PATH (covers CI where uv is installed to a tool cache dir),
310
+ # then common fixed locations.
311
+ found = shutil.which("uv")
312
+ if found:
313
+ return Path(found)
314
+
315
+ for candidate in (
316
+ Path("/opt/homebrew/bin/uv"),
317
+ Path.home() / ".local" / "bin" / "uv",
318
+ ):
319
+ if candidate.exists():
320
+ return candidate
321
+
322
+ raise RuntimeError("uv not found in your PATH. Please install it and try again.")
319
323
 
320
324
 
321
325
  def call_with_uv(
@@ -662,7 +662,9 @@ def test_app_publish_processor_type_without_ui_skips_ui_export(monkeypatch, tmp_
662
662
  assert "export_ui_called" not in captured
663
663
  assert captured["build_script"] == ("./build.sh", tmp_path)
664
664
  assert captured["processor_source"] == ("303", {"file": package_fp})
665
- assert renderer.render_calls == [{"id": 303, "versioned": True}]
665
+ # publish uploads the package but no longer cuts a version (that's `release`)
666
+ assert "processor_version" not in captured
667
+ assert renderer.render_calls == [{"id": 303}]
666
668
 
667
669
 
668
670
  def test_app_publish_skips_ui_export_when_config_disables_generation(
@@ -769,9 +771,7 @@ def test_app_publish_honours_explicit_staging(monkeypatch, tmp_path):
769
771
  lambda ctx, app_fp, validate_: None,
770
772
  )
771
773
 
772
- result = runner.invoke(
773
- app, ["app", "publish", str(tmp_path), "--staging"]
774
- )
774
+ result = runner.invoke(app, ["app", "publish", str(tmp_path), "--staging"])
775
775
 
776
776
  assert result.exit_code == 0
777
777
  assert captured["body"]["deployment_data"] == "staging-data"
@@ -872,9 +872,7 @@ def test_app_publish_skips_build_when_requested_by_config(monkeypatch, tmp_path)
872
872
  assert renderer.render_calls == [{"id": 101}]
873
873
 
874
874
 
875
- def test_app_publish_processor_builds_package_and_releases_version(
876
- monkeypatch, tmp_path
877
- ):
875
+ def test_app_publish_processor_builds_package_without_release(monkeypatch, tmp_path):
878
876
  captured = {}
879
877
  renderer = FakeRenderer()
880
878
  app_config = FakeAppConfig(app_id=303)
@@ -938,10 +936,11 @@ def test_app_publish_processor_builds_package_and_releases_version(
938
936
  assert captured["build_script"] == ("./build.sh", tmp_path)
939
937
  assert captured["application_id"] == "303"
940
938
  assert captured["body"] == {"file": package_fp}
941
- assert captured["version"][0] == "303"
939
+ # publish uploads the package; the version is cut by `doover app release`
940
+ assert "version" not in captured
942
941
  assert "build_called" not in captured
943
942
  assert "push_called" not in captured
944
- assert renderer.render_calls == [{"id": 303, "versioned": True}]
943
+ assert renderer.render_calls == [{"id": 303}]
945
944
 
946
945
 
947
946
  def test_publish_processor_package_uses_package_zip(tmp_path):
@@ -187,3 +187,49 @@ def test_session_require_agent_id_rejects_missing_value():
187
187
 
188
188
  with pytest.raises(typer.BadParameter):
189
189
  session.require_agent_id(None)
190
+
191
+
192
+ def test_trusted_publisher_provider_detects_github_actions(monkeypatch):
193
+ from doover_cli.utils.api import _trusted_publisher_provider
194
+
195
+ monkeypatch.delenv("DOOVER_TRUSTED_PUBLISHER", raising=False)
196
+ monkeypatch.setenv("GITHUB_ACTIONS", "true")
197
+ monkeypatch.setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "rt")
198
+
199
+ assert _trusted_publisher_provider() == "GH"
200
+
201
+
202
+ def test_trusted_publisher_provider_explicit_flag(monkeypatch):
203
+ from doover_cli.utils.api import _trusted_publisher_provider
204
+
205
+ monkeypatch.setenv("DOOVER_TRUSTED_PUBLISHER", "1")
206
+
207
+ assert _trusted_publisher_provider() == "GH"
208
+
209
+
210
+ def test_trusted_publisher_provider_absent(monkeypatch):
211
+ from doover_cli.utils.api import _trusted_publisher_provider
212
+
213
+ monkeypatch.delenv("DOOVER_TRUSTED_PUBLISHER", raising=False)
214
+ monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
215
+ monkeypatch.delenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", raising=False)
216
+
217
+ assert _trusted_publisher_provider() is None
218
+
219
+
220
+ def test_session_from_trusted_publisher_builds_control_client():
221
+ from pydoover.api import auth as _auth
222
+
223
+ if not hasattr(_auth, "TrustedPublisherAuthClient"):
224
+ pytest.skip("installed pydoover lacks trusted-publisher support yet")
225
+
226
+ session = DooverCLISession.from_trusted_publisher(
227
+ provider="GH",
228
+ oidc_token="gh-oidc",
229
+ control_base_url="https://control.example",
230
+ )
231
+
232
+ assert session.auth.provider == "GH"
233
+ assert session.auth.control_base_url == "https://control.example"
234
+ client = session.get_control_client()
235
+ assert client.base_url == "https://control.example"