orchestra-cli 0.5.0__tar.gz → 0.5.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 (61) hide show
  1. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/PKG-INFO +1 -1
  2. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/migrate_pipeline.py +3 -1
  3. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/run_pipeline.py +29 -4
  4. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_import_pipeline.py +2 -0
  5. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_run_pipeline.py +94 -0
  6. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/utils/test_git.py +43 -0
  7. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/git.py +25 -2
  8. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/PKG-INFO +1 -1
  9. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/pyproject.toml +1 -1
  10. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/README.md +0 -0
  11. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/__init__.py +0 -0
  12. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/build_pipeline.py +0 -0
  13. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/cli.py +0 -0
  14. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/create_pipeline.py +0 -0
  15. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/delete_pipeline.py +0 -0
  16. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/fetch_pipelines.py +0 -0
  17. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/get_pipeline.py +0 -0
  18. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/import_pipeline.py +0 -0
  19. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/list_accounts.py +0 -0
  20. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/login.py +0 -0
  21. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/login_page.html +0 -0
  22. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/pipeline_upsert.py +0 -0
  23. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/task_logs.py +0 -0
  24. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/update_pipeline.py +0 -0
  25. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/use_account.py +0 -0
  26. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/src/validate_pipeline.py +0 -0
  27. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/__init__.py +0 -0
  28. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/conftest.py +0 -0
  29. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_build_pipeline.py +0 -0
  30. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_cli.py +0 -0
  31. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_create_pipeline.py +0 -0
  32. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_delete_pipeline.py +0 -0
  33. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_fetch_pipelines.py +0 -0
  34. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_get_pipeline.py +0 -0
  35. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_list_accounts.py +0 -0
  36. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_login.py +0 -0
  37. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_migrate_pipeline.py +0 -0
  38. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_pipeline_selector.py +0 -0
  39. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_task_logs.py +0 -0
  40. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_update_pipeline.py +0 -0
  41. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_use_account.py +0 -0
  42. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/test_validate_pipeline.py +0 -0
  43. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/utils/__init__.py +0 -0
  44. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/utils/test_api.py +0 -0
  45. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/tests/utils/test_picker.py +0 -0
  46. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/accounts.py +0 -0
  47. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/api.py +0 -0
  48. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/constants.py +0 -0
  49. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/credentials.py +0 -0
  50. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/picker.py +0 -0
  51. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/pipeline_lookup.py +0 -0
  52. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/pipeline_selector.py +0 -0
  53. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/pipeline_update.py +0 -0
  54. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/styling.py +0 -0
  55. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli/utils/yaml_loader.py +0 -0
  56. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/SOURCES.txt +0 -0
  57. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/dependency_links.txt +0 -0
  58. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/entry_points.txt +0 -0
  59. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/requires.txt +0 -0
  60. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/orchestra_cli.egg-info/top_level.txt +0 -0
  61. {orchestra_cli-0.5.0 → orchestra_cli-0.5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestra-cli
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Orchestra CLI. Perform operations with Orchestra locally.
5
5
  Author-email: Orchestra Technologies <support@getorchestra.io>
6
6
  Requires-Python: >=3.10
@@ -364,7 +364,8 @@ def migrate_pipeline(
364
364
  typer.echo(red("Could not detect repository URL from git"))
365
365
  raise typer.Exit(code=1)
366
366
 
367
- storage_provider_value = detect_storage_provider(get_remote_url(repo_root))
367
+ remote_url = get_remote_url(repo_root)
368
+ storage_provider_value = detect_storage_provider(remote_url)
368
369
  if not storage_provider_value:
369
370
  typer.echo(red("Could not detect storage provider - no matching host"))
370
371
  raise typer.Exit(code=1)
@@ -437,6 +438,7 @@ def migrate_pipeline(
437
438
  repository_slug,
438
439
  default_branch,
439
440
  pushed_branch,
441
+ remote_url,
440
442
  )
441
443
  if compare_link:
442
444
  typer.echo(yellow(f"Open PR / compare link: {compare_link}"))
@@ -37,13 +37,14 @@ STATUS_STYLES = {
37
37
  "CREATED": "cyan",
38
38
  "QUEUED": "yellow",
39
39
  "RUNNING": "blue",
40
+ "CANCELLING": "red",
40
41
  "SUCCEEDED": "green",
41
42
  "WARNING": "yellow",
42
43
  "SKIPPED": "yellow",
43
44
  "FAILED": "red",
44
45
  "CANCELLED": "red",
45
46
  }
46
- IN_PROGRESS_RUN_STATUSES = {"RUNNING", "QUEUED", "CREATED"}
47
+ IN_PROGRESS_RUN_STATUSES = {"RUNNING", "QUEUED", "CREATED", "CANCELLING"}
47
48
  TERMINAL_RUN_STATUSES = {"SUCCEEDED", "WARNING", "SKIPPED", "FAILED", "CANCELLED"}
48
49
 
49
50
 
@@ -307,13 +308,23 @@ def _format_pipeline_duration(created_at_utc: datetime, now_utc: datetime | None
307
308
  return f"{seconds} {unit}"
308
309
 
309
310
 
311
+ def _run_message(status_body: dict[str, object]) -> str | None:
312
+ message = status_body.get("message")
313
+ if isinstance(message, str) and message.strip():
314
+ return message.strip()
315
+ return None
316
+
317
+
310
318
  def _build_poll_status_text(
311
319
  status_value: str,
312
320
  created_at_utc: datetime | None = None,
313
321
  now_utc: datetime | None = None,
322
+ status_message: str | None = None,
314
323
  ) -> Text:
315
324
  text = Text("Pipeline status: ")
316
325
  text.append(status_value, style=f"bold {STATUS_STYLES.get(status_value, 'white')}")
326
+ if status_message is not None:
327
+ text.append(f" - {status_message}")
317
328
  if created_at_utc is not None:
318
329
  text.append(f" ({_format_pipeline_duration(created_at_utc, now_utc)})", style="dim")
319
330
  return text
@@ -419,10 +430,11 @@ def _build_poll_display(
419
430
  task_runs: list[dict[str, object]],
420
431
  can_fetch_task_runs: bool,
421
432
  now_utc: datetime | None = None,
433
+ status_message: str | None = None,
422
434
  ) -> Group:
423
435
  return Group(
424
436
  _build_live_status_text(seconds_since_check),
425
- _build_poll_status_text(status_value, created_at_utc, now_utc),
437
+ _build_poll_status_text(status_value, created_at_utc, now_utc, status_message),
426
438
  _build_task_runs_table(
427
439
  task_runs,
428
440
  can_fetch_task_runs=can_fetch_task_runs,
@@ -452,6 +464,7 @@ def _sleep_with_status_updates(
452
464
  created_at_utc: datetime | None,
453
465
  task_runs: list[dict[str, object]],
454
466
  can_fetch_task_runs: bool,
467
+ status_message: str | None = None,
455
468
  sleep_fn: Callable[[float], None] | None = None,
456
469
  ) -> None:
457
470
  sleep_fn = sleep_fn or time.sleep
@@ -468,6 +481,7 @@ def _sleep_with_status_updates(
468
481
  created_at_utc=created_at_utc,
469
482
  task_runs=task_runs,
470
483
  can_fetch_task_runs=can_fetch_task_runs,
484
+ status_message=status_message,
471
485
  ),
472
486
  )
473
487
 
@@ -489,6 +503,7 @@ def _poll_until_terminal(
489
503
  status_url = get_api_url(f"pipeline_runs/{pipeline_run_id}/status")
490
504
  poll_status_display = _create_poll_status_display()
491
505
  last_status_value: str | None = None
506
+ last_status_message: str | None = None
492
507
  last_task_runs: list[dict[str, object]] = []
493
508
  can_fetch_task_runs = False
494
509
  missing_run_status_polls = 0
@@ -502,6 +517,7 @@ def _poll_until_terminal(
502
517
  created_at_utc=created_at_utc,
503
518
  task_runs=last_task_runs,
504
519
  can_fetch_task_runs=can_fetch_task_runs,
520
+ status_message=last_status_message,
505
521
  )
506
522
  try:
507
523
  status_resp = client.get(status_url)
@@ -530,11 +546,13 @@ def _poll_until_terminal(
530
546
  status_body = {}
531
547
 
532
548
  status_value = status_body.get("runStatus")
549
+ run_message = _run_message(status_body)
533
550
  created_at_utc = created_at_utc or _parse_created_at_utc(status_body)
534
551
 
535
552
  if isinstance(status_value, str):
536
553
  missing_run_status_polls = 0
537
554
  last_status_value = status_value
555
+ last_status_message = run_message if status_value == "QUEUED" else None
538
556
  can_fetch_task_runs = status_value in (
539
557
  IN_PROGRESS_RUN_STATUSES | TERMINAL_RUN_STATUSES
540
558
  )
@@ -548,10 +566,14 @@ def _poll_until_terminal(
548
566
  created_at_utc=created_at_utc,
549
567
  task_runs=last_task_runs,
550
568
  can_fetch_task_runs=can_fetch_task_runs,
569
+ status_message=last_status_message,
551
570
  ),
552
571
  )
553
572
  else:
554
- typer.echo(f"Pipeline ({selector_name}) status: {status_value}")
573
+ status_line = f"Pipeline ({selector_name}) status: {status_value}"
574
+ if last_status_message is not None:
575
+ status_line += f" - {last_status_message}"
576
+ typer.echo(status_line)
555
577
  else:
556
578
  missing_run_status_polls += 1
557
579
  if missing_run_status_polls <= max_missing_run_status_polls:
@@ -572,7 +594,10 @@ def _poll_until_terminal(
572
594
  if status_value == "SKIPPED":
573
595
  _stop_poll_status_display(poll_status_display)
574
596
  poll_status_display = None
575
- typer.echo(yellow("⚠ Pipeline skipped"))
597
+ skipped_text = (
598
+ f"⚠ Pipeline skipped: {run_message}" if run_message else "⚠ Pipeline skipped"
599
+ )
600
+ typer.echo(yellow(skipped_text))
576
601
  raise typer.Exit(code=0)
577
602
 
578
603
  if status_value in {"FAILED", "CANCELLED"}:
@@ -36,6 +36,8 @@ def mock_env(monkeypatch):
36
36
  [
37
37
  ("https://github.com/org/repo.git", "GITHUB", "org/repo"),
38
38
  ("git@github.com:org/repo.git", "GITHUB", "org/repo"),
39
+ ("https://acme.ghe.com/org/repo.git", "GITHUB", "org/repo"),
40
+ ("git@acme.ghe.com:org/repo.git", "GITHUB", "org/repo"),
39
41
  ("https://gitlab.com/org/repo.git", "GITLAB", "org/repo"),
40
42
  ("git@gitlab.com:org/repo.git", "GITLAB", "org/repo"),
41
43
  ("https://dev.azure.com/org/project/_git/repo", "AZURE_DEVOPS", "project/repo"),
@@ -1,3 +1,5 @@
1
+ import subprocess
2
+ import time
1
3
  from datetime import UTC, datetime
2
4
  from pathlib import Path
3
5
 
@@ -96,6 +98,11 @@ def test_build_poll_status_text_includes_duration() -> None:
96
98
  assert text.plain == "Pipeline status: RUNNING (1:30 minutes)"
97
99
 
98
100
 
101
+ def test_build_poll_status_text_includes_status_message() -> None:
102
+ text = _build_poll_status_text("QUEUED", status_message="Waiting for a free slot")
103
+ assert text.plain == "Pipeline status: QUEUED - Waiting for a free slot"
104
+
105
+
99
106
  def test_build_live_status_text_includes_update_age() -> None:
100
107
  text = _build_live_status_text(3)
101
108
  assert text.plain == "Live status (updated 3 seconds ago)"
@@ -1094,6 +1101,93 @@ def test_run_wait_warning(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
1094
1101
  assert result.output.strip().splitlines()[-1] == "⚠ Pipeline completed with warnings"
1095
1102
 
1096
1103
 
1104
+ def _invoke_run_through_statuses(
1105
+ httpx_mock: HTTPXMock,
1106
+ monkeypatch,
1107
+ tmp_path: Path,
1108
+ status_bodies: list[dict[str, str]],
1109
+ ):
1110
+ mapping = {
1111
+ ("rev-parse", "--show-toplevel"): (0, str(tmp_path), ""),
1112
+ ("status", "--porcelain"): (0, "", ""),
1113
+ ("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"): (1, "", ""),
1114
+ }
1115
+ monkeypatch.setattr(time, "sleep", lambda _: None)
1116
+ monkeypatch.setattr(subprocess, "run", make_git_subprocess_mock(mapping))
1117
+
1118
+ base = "https://app.getorchestra.io/api/engine/public"
1119
+ httpx_mock.add_response(
1120
+ method="POST",
1121
+ url=f"{base}/pipelines/demo/start",
1122
+ json={"pipelineRunId": "run-q"},
1123
+ )
1124
+ for status_body in status_bodies:
1125
+ httpx_mock.add_response(
1126
+ method="GET",
1127
+ url=f"{base}/pipeline_runs/run-q/status",
1128
+ json={**status_body, "pipelineName": "demo"},
1129
+ )
1130
+ httpx_mock.add_response(
1131
+ method="GET",
1132
+ url=f"{base}/pipeline_runs/run-q/task_runs?page_size=50&page=1",
1133
+ json={"results": []},
1134
+ )
1135
+
1136
+ return runner.invoke(app, ["pipeline", "run", "--alias", "demo", "--wait"])
1137
+
1138
+
1139
+ def test_run_wait_keeps_polling_while_cancelling(
1140
+ httpx_mock: HTTPXMock,
1141
+ monkeypatch,
1142
+ tmp_path: Path,
1143
+ ):
1144
+ result = _invoke_run_through_statuses(
1145
+ httpx_mock,
1146
+ monkeypatch,
1147
+ tmp_path,
1148
+ [{"runStatus": "QUEUED"}, {"runStatus": "CANCELLING"}, {"runStatus": "CANCELLED"}],
1149
+ )
1150
+
1151
+ assert result.exit_code == 1
1152
+ assert "Invalid status value" not in result.output
1153
+ assert "status: CANCELLING" in result.output
1154
+ assert "status CANCELLED" in result.output
1155
+
1156
+
1157
+ def test_run_wait_shows_queued_message_then_final_status(
1158
+ httpx_mock: HTTPXMock,
1159
+ monkeypatch,
1160
+ tmp_path: Path,
1161
+ ):
1162
+ result = _invoke_run_through_statuses(
1163
+ httpx_mock,
1164
+ monkeypatch,
1165
+ tmp_path,
1166
+ [
1167
+ {"runStatus": "QUEUED", "message": "Waiting for a free slot"},
1168
+ {"runStatus": "RUNNING", "message": "Waiting for a free slot"},
1169
+ {"runStatus": "SUCCEEDED"},
1170
+ ],
1171
+ )
1172
+
1173
+ assert result.exit_code == 0
1174
+ assert "Pipeline (alias: demo) status: QUEUED - Waiting for a free slot" in result.output
1175
+ assert "Pipeline (alias: demo) status: RUNNING\n" in result.output
1176
+ assert result.output.strip().splitlines()[-1] == "✅ Pipeline succeeded"
1177
+
1178
+
1179
+ def test_run_wait_skipped_prints_reason(httpx_mock: HTTPXMock, monkeypatch, tmp_path: Path):
1180
+ result = _invoke_run_through_statuses(
1181
+ httpx_mock,
1182
+ monkeypatch,
1183
+ tmp_path,
1184
+ [{"runStatus": "SKIPPED", "message": "Replaced by a newer run"}],
1185
+ )
1186
+
1187
+ assert result.exit_code == 0
1188
+ assert result.output.strip().splitlines()[-1] == "⚠ Pipeline skipped: Replaced by a newer run"
1189
+
1190
+
1097
1191
  def test_run_git_backed_path_uses_prepared_branch_and_commit(
1098
1192
  httpx_mock: HTTPXMock,
1099
1193
  monkeypatch,
@@ -114,3 +114,46 @@ def test_require_repo_root_walks_up_from_nonexistent_parent(monkeypatch, tmp_pat
114
114
  monkeypatch.setattr(subprocess, "run", make_git_subprocess_mock(mapping))
115
115
 
116
116
  assert git_module.require_repo_root(target_file, git_module.GitAction.MIGRATE) == tmp_path
117
+
118
+
119
+ @pytest.mark.parametrize(
120
+ "remote_url, expected",
121
+ [
122
+ ("https://github.com/org/repo.git", "GITHUB"),
123
+ ("git@github.com:org/repo.git", "GITHUB"),
124
+ ("https://acme.ghe.com/org/repo.git", "GITHUB"),
125
+ ("https://user:token@acme.ghe.com/org/repo.git", "GITHUB"),
126
+ ("git@acme.ghe.com:org/repo.git", "GITHUB"),
127
+ ("ssh://git@acme.ghe.com/org/repo.git", "GITHUB"),
128
+ ("https://gitlab.com/org/repo.git", "GITLAB"),
129
+ ("https://git.example.com/org/repo.git", None),
130
+ ("https://example.com/ghe.com/repo.git", None),
131
+ ("https://[malformed/org/repo.git", None),
132
+ (None, None),
133
+ ],
134
+ )
135
+ def test_detect_storage_provider(remote_url: str | None, expected: str | None):
136
+ assert git_module.detect_storage_provider(remote_url) == expected
137
+
138
+
139
+ @pytest.mark.parametrize(
140
+ "remote_url, expected",
141
+ [
142
+ (
143
+ "git@github.com:org/repo.git",
144
+ "https://github.com/org/repo/compare/main...feature?expand=1",
145
+ ),
146
+ (
147
+ "git@acme.ghe.com:org/repo.git",
148
+ "https://acme.ghe.com/org/repo/compare/main...feature?expand=1",
149
+ ),
150
+ (
151
+ "https://user:token@acme.ghe.com/org/repo.git",
152
+ "https://acme.ghe.com/org/repo/compare/main...feature?expand=1",
153
+ ),
154
+ (None, "https://github.com/org/repo/compare/main...feature?expand=1"),
155
+ ],
156
+ )
157
+ def test_build_compare_link_github(remote_url: str | None, expected: str):
158
+ link = git_module.build_compare_link("GITHUB", "org/repo", "main", "feature", remote_url)
159
+ assert link == expected
@@ -4,11 +4,14 @@ import string
4
4
  import subprocess
5
5
  from enum import StrEnum
6
6
  from pathlib import Path
7
+ from urllib.parse import urlsplit
7
8
 
8
9
  import typer
9
10
 
10
11
  from .styling import bold, red, yellow
11
12
 
13
+ GHE_COM_HOST_SUFFIX = ".ghe.com"
14
+
12
15
 
13
16
  class GitAction(StrEnum):
14
17
  BUILD = "Build"
@@ -84,12 +87,30 @@ def detect_current_branch(repo_root: Path, allow_detached: bool = True) -> str |
84
87
  return out
85
88
 
86
89
 
90
+ def _remote_host(remote_url: str) -> str | None:
91
+ """The host of a URL remote, or of the SSH shorthand ``[user@]host:path``."""
92
+ remote_url = remote_url.strip()
93
+ if "://" in remote_url:
94
+ try:
95
+ return urlsplit(remote_url).hostname
96
+ except ValueError:
97
+ return None
98
+ user_and_host, separator, _ = remote_url.partition(":")
99
+ return user_and_host.rpartition("@")[2].lower() if separator else None
100
+
101
+
102
+ def _ghe_com_host(remote_url: str | None) -> str | None:
103
+ """The remote's host if it's on GitHub Enterprise Cloud with data residency."""
104
+ host = _remote_host(remote_url) if remote_url else None
105
+ return host if host and host.endswith(GHE_COM_HOST_SUFFIX) else None
106
+
107
+
87
108
  def detect_storage_provider(repository_url: str | None) -> str | None:
88
109
  if not repository_url:
89
110
  return None
90
111
 
91
112
  url = repository_url.lower()
92
- if "github.com" in url:
113
+ if "github.com" in url or _ghe_com_host(repository_url):
93
114
  return "GITHUB"
94
115
  if "gitlab.com" in url:
95
116
  return "GITLAB"
@@ -103,10 +124,12 @@ def build_compare_link(
103
124
  repository_slug: str,
104
125
  default_branch: str,
105
126
  branch_name: str,
127
+ remote_url: str | None,
106
128
  ) -> str | None:
107
129
  provider = storage_provider.upper()
108
130
  if provider == "GITHUB":
109
- return f"https://github.com/{repository_slug}/compare/{default_branch}...{branch_name}?expand=1"
131
+ host = _ghe_com_host(remote_url) or "github.com"
132
+ return f"https://{host}/{repository_slug}/compare/{default_branch}...{branch_name}?expand=1"
110
133
  if provider == "GITLAB":
111
134
  return f"https://gitlab.com/{repository_slug}/-/compare/{default_branch}...{branch_name}"
112
135
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestra-cli
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Orchestra CLI. Perform operations with Orchestra locally.
5
5
  Author-email: Orchestra Technologies <support@getorchestra.io>
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "orchestra-cli"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Orchestra CLI. Perform operations with Orchestra locally."
5
5
  readme = "README.md"
6
6
  authors = [
File without changes
File without changes