dagster-cloud-cli 1.13.26__tar.gz → 1.13.28__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 (93) hide show
  1. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/PKG-INFO +3 -2
  2. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/ci/__init__.py +18 -1
  3. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/ci/state.py +8 -0
  4. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config_utils.py +7 -0
  5. dagster_cloud_cli-1.13.28/dagster_cloud_cli/core/git_repo_context.py +168 -0
  6. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/deps.py +4 -7
  7. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/workspace.py +14 -1
  8. dagster_cloud_cli-1.13.28/dagster_cloud_cli/version.py +1 -0
  9. dagster_cloud_cli-1.13.28/dagster_cloud_cli_tests/core_tests/test_git_repo_context.py +179 -0
  10. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_ci_commands.py +14 -0
  11. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_pyproject_toml_support.py +1 -1
  12. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/pyproject.toml +3 -2
  13. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/uv.lock +35 -0
  14. dagster_cloud_cli-1.13.26/dagster_cloud_cli/version.py +0 -1
  15. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/.gitignore +0 -0
  16. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/LICENSE +0 -0
  17. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/__init__.py +0 -0
  18. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/__init__.py +0 -0
  19. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/branch_deployment/__init__.py +0 -0
  20. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/ci/checks.py +0 -0
  21. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/ci/report.py +0 -0
  22. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/ci/utils.py +0 -0
  23. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/config.py +0 -0
  24. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/deployment/__init__.py +0 -0
  25. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/deployment/alert_policies/__init__.py +0 -0
  26. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/deployment/alert_policies/commands.py +0 -0
  27. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/integration/__init__.py +0 -0
  28. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/integration/atlan/__init__.py +0 -0
  29. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/job/__init__.py +0 -0
  30. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/metrics.py +0 -0
  31. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/organization/__init__.py +0 -0
  32. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/organization/saml/__init__.py +0 -0
  33. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/organization/saml/commands.py +0 -0
  34. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/run/__init__.py +0 -0
  35. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/serverless/Dockerfile +0 -0
  36. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/serverless/Dockerfile.dagster-cloud-cli +0 -0
  37. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/serverless/__init__.py +0 -0
  38. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/serverless/pex_bundle.Dockerfile +0 -0
  39. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/commands/workspace/__init__.py +0 -0
  40. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/__init__.py +0 -0
  41. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/jinja_template_loader.py +0 -0
  42. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/list_merge_strategies.py +0 -0
  43. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/loader.py +0 -0
  44. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/merger.py +0 -0
  45. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/config/models.py +0 -0
  46. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/__init__.py +0 -0
  47. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/agent_queue.py +0 -0
  48. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/alert_types.py +0 -0
  49. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/artifacts.py +0 -0
  50. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/docker_runner.py +0 -0
  51. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/errors.py +0 -0
  52. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/graphql_client.py +0 -0
  53. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/headers/__init__.py +0 -0
  54. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/headers/auth.py +0 -0
  55. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/headers/impl.py +0 -0
  56. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/headers/versioning/__init__.py +0 -0
  57. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/headers/versioning/constants.py +0 -0
  58. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/__init__.py +0 -0
  59. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/code_location.py +0 -0
  60. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/deploy.py +0 -0
  61. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/git_context.py +0 -0
  62. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/github_context.py +0 -0
  63. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/gitlab_context.py +0 -0
  64. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/parse_workspace.py +0 -0
  65. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/pex_registry.py +0 -0
  66. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/platforms.py +0 -0
  67. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/selftest.py +0 -0
  68. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/source.py +0 -0
  69. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/core/pex_builder/util.py +0 -0
  70. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/docker_utils.py +0 -0
  71. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/entrypoint.py +0 -0
  72. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/gql.py +0 -0
  73. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/pex_utils.py +0 -0
  74. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/py.typed +0 -0
  75. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/types.py +0 -0
  76. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/ui.py +0 -0
  77. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli/utils.py +0 -0
  78. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/__init__.py +0 -0
  79. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/commands/ci/integration/__init__.py +0 -0
  80. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/commands/ci/test_ci_template.py +0 -0
  81. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/commands/integration/test_atlan_integration_cli.py +0 -0
  82. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/config/test_config_loader.py +0 -0
  83. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/config/test_defaults_merging.py +0 -0
  84. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/config/test_template_loader.py +0 -0
  85. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/conftest.py +0 -0
  86. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_artifacts.py +0 -0
  87. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_check.py +0 -0
  88. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_gql.py +0 -0
  89. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_metrics.py +0 -0
  90. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_pex_bundle_marker.py +0 -0
  91. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_serverless_v2_redirect.py +0 -0
  92. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/dagster_cloud_cli_tests/test_utils.py +0 -0
  93. {dagster_cloud_cli-1.13.26 → dagster_cloud_cli-1.13.28}/tox.ini +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dagster-cloud-cli
3
- Version: 1.13.26
3
+ Version: 1.13.28
4
4
  Project-URL: Homepage, https://github.com/dagster-io/dagster/tree/master/python_modules/libraries/dagster-cloud-cli
5
5
  Author-email: Dagster Labs <hello@dagsterlabs.com>
6
6
  License-Expression: Apache-2.0
7
7
  License-File: LICENSE
8
8
  Requires-Python: <3.15,>=3.10
9
9
  Requires-Dist: click
10
- Requires-Dist: dagster-shared==1.13.26
10
+ Requires-Dist: dagster-shared==1.13.28
11
11
  Requires-Dist: github3-py
12
+ Requires-Dist: gitpython
12
13
  Requires-Dist: jinja2
13
14
  Requires-Dist: packaging>=20.9
14
15
  Requires-Dist: pyyaml>=5.1
@@ -37,7 +37,7 @@ from dagster_cloud_cli.config_utils import (
37
37
  get_org_url,
38
38
  get_user_token,
39
39
  )
40
- from dagster_cloud_cli.core import pex_builder
40
+ from dagster_cloud_cli.core import git_repo_context, pex_builder
41
41
  from dagster_cloud_cli.core.artifacts import (
42
42
  download_organization_artifact,
43
43
  upload_organization_artifact,
@@ -446,7 +446,14 @@ def init_impl(
446
446
  status_url: str | None,
447
447
  snapshot_base_condition: SnapshotBaseDeploymentCondition | None,
448
448
  require_branch_deployment: bool,
449
+ defs_base_path: str | None = None,
449
450
  ):
451
+ # Best-effort: a deploy that isn't run from a checkout reports nothing, and
452
+ # the server falls back to whatever binding was set by hand.
453
+ repo_owner, repo_name = git_repo_context.discover_repo_owner_and_name(project_dir)
454
+ base_branch = git_repo_context.discover_base_branch(project_dir)
455
+ git_provider = git_repo_context.discover_git_provider(project_dir)
456
+
450
457
  yaml_path = pathlib.Path(project_dir) / dagster_cloud_yaml_path
451
458
  if not yaml_path.exists():
452
459
  raise ui.error(
@@ -525,6 +532,11 @@ def init_impl(
525
532
  git_url=git_url,
526
533
  commit_hash=commit_hash,
527
534
  build_config=location.build,
535
+ git_provider=git_provider,
536
+ repo_owner=repo_owner,
537
+ repo_name=repo_name,
538
+ base_branch=base_branch,
539
+ defs_base_path=defs_base_path,
528
540
  ),
529
541
  build_output=None,
530
542
  status_url=status_url,
@@ -1233,6 +1245,11 @@ def _deploy(
1233
1245
  "location_file": location_state.location_file,
1234
1246
  "git_url": location_state.build.git_url,
1235
1247
  "commit_hash": location_state.build.commit_hash,
1248
+ "git_provider": location_state.build.git_provider,
1249
+ "repo_owner": location_state.build.repo_owner,
1250
+ "repo_name": location_state.build.repo_name,
1251
+ "base_branch": location_state.build.base_branch,
1252
+ "defs_base_path": location_state.build.defs_base_path,
1236
1253
  **(
1237
1254
  {"defs_state_info": location_state.defs_state_info.model_dump()}
1238
1255
  if location_state.defs_state_info
@@ -32,6 +32,14 @@ class BuildMetadata(BaseModel):
32
32
  git_url: str | None
33
33
  commit_hash: str | None
34
34
  build_config: models.Build | None # copied from dagster_cloud.yaml
35
+ # Where UI-authored components belong. Written at session init and read back
36
+ # by ``finish`` in a separate process, so these have to live in the statedir.
37
+ # Default to None so a statedir written before these existed still loads.
38
+ git_provider: str | None = None
39
+ repo_owner: str | None = None
40
+ repo_name: str | None = None
41
+ base_branch: str | None = None
42
+ defs_base_path: str | None = None
35
43
 
36
44
 
37
45
  class LocationStatus(Enum):
@@ -495,6 +495,13 @@ def get_location_document(name: str | None, kwargs: dict[str, Any]) -> dict[str,
495
495
  "git": {
496
496
  "commit_hash": kwargs.get("commit_hash"),
497
497
  "url": kwargs.get("git_url"),
498
+ # Where this location's UI-authored components belong. Absent for
499
+ # deploys from CLIs that predate them.
500
+ "provider": kwargs.get("git_provider"),
501
+ "repo_owner": kwargs.get("repo_owner"),
502
+ "repo_name": kwargs.get("repo_name"),
503
+ "base_branch": kwargs.get("base_branch"),
504
+ "defs_base_path": kwargs.get("defs_base_path"),
498
505
  },
499
506
  "pex_metadata": (
500
507
  {
@@ -0,0 +1,168 @@
1
+ """Discover which repo, and which branch, a deploy is running against.
2
+
3
+ Reported with the deploy so the server knows where a code location's UI-authored
4
+ components belong. Everything here is best-effort: a deploy that isn't run from
5
+ a checkout, or has no remote, simply reports nothing.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import re
12
+ from typing import TYPE_CHECKING
13
+ from urllib.parse import urlparse
14
+
15
+ if TYPE_CHECKING:
16
+ from git import Repo
17
+
18
+ logger = logging.getLogger("dagster_cloud_cli")
19
+
20
+ # git@github.com:acme/proj.git, https://github.com/acme/proj.git, ssh://git@host/acme/proj.
21
+ # The owner may span several segments: GitLab namespaces nest (``group/subgroup``).
22
+ _REMOTE_PATTERN = re.compile(
23
+ r"^(?:[a-z+]+://[^/]+/|[^@/:]+@[^:/]+:)(?P<owner>.+)/(?P<repo>[^/]+?)(?:\.git)?/?$"
24
+ )
25
+
26
+
27
+ def _open_repo(project_dir: str) -> "Repo | None":
28
+ try:
29
+ # Imported lazily: GitPython raises at import time when no git binary is on PATH.
30
+ from git import InvalidGitRepositoryError, NoSuchPathError, Repo
31
+ except ImportError:
32
+ logger.debug("git is unavailable", exc_info=True)
33
+ return None
34
+ try:
35
+ return Repo(project_dir, search_parent_directories=True)
36
+ except (InvalidGitRepositoryError, NoSuchPathError):
37
+ return None
38
+
39
+
40
+ def parse_owner_and_repo(remote_url: str) -> tuple[str, str] | None:
41
+ """``(owner, repo)`` from a git remote URL, or None if it isn't one."""
42
+ match = _REMOTE_PATTERN.search(remote_url.strip())
43
+ if not match:
44
+ return None
45
+ return match.group("owner"), match.group("repo")
46
+
47
+
48
+ def discover_repo_owner_and_name(project_dir: str) -> tuple[str | None, str | None]:
49
+ """The owner and repo this deploy is running against.
50
+
51
+ Prefers ``GITHUB_REPOSITORY`` / ``CI_PROJECT_PATH`` (``owner/repo``), which
52
+ CI sets from the provider's own event rather than from anything a user typed.
53
+ """
54
+ from_env = os.environ.get("GITHUB_REPOSITORY") or os.environ.get("CI_PROJECT_PATH")
55
+ if from_env and "/" in from_env:
56
+ owner, _, name = from_env.rpartition("/")
57
+ return owner, name
58
+
59
+ remote = _origin_url(project_dir)
60
+ parsed = parse_owner_and_repo(remote) if remote else None
61
+ return parsed if parsed else (None, None)
62
+
63
+
64
+ def discover_git_provider(project_dir: str) -> str | None:
65
+ """``"github"`` or ``"gitlab"``, or None when it can't be told.
66
+
67
+ Taken from the CI system when there is one, since a self-hosted GitLab's
68
+ hostname need not mention GitLab; otherwise from the origin remote's host.
69
+ """
70
+ if os.environ.get("GITLAB_CI"):
71
+ return "gitlab"
72
+ if os.environ.get("GITHUB_ACTIONS"):
73
+ return "github"
74
+
75
+ remote = _origin_url(project_dir)
76
+ host = _remote_host(remote) if remote else ""
77
+ if "gitlab" in host:
78
+ return "gitlab"
79
+ if "github" in host:
80
+ return "github"
81
+ return None
82
+
83
+
84
+ def _origin_url(project_dir: str) -> str | None:
85
+ repo = _open_repo(project_dir)
86
+ if repo is None:
87
+ return None
88
+ from git import GitCommandError
89
+
90
+ try:
91
+ return repo.remote("origin").url
92
+ except (ValueError, GitCommandError):
93
+ return None
94
+
95
+
96
+ def _remote_host(remote_url: str) -> str:
97
+ if "://" in remote_url:
98
+ return (urlparse(remote_url).hostname or "").lower()
99
+ # scp-like syntax, e.g. git@gitlab.com:acme/proj.git
100
+ return remote_url.rpartition("@")[2].partition(":")[0].lower()
101
+
102
+
103
+ def _github_default_branch() -> str | None:
104
+ """The repo's default branch, from the GitHub Actions event payload."""
105
+ event_path = os.environ.get("GITHUB_EVENT_PATH")
106
+ if not event_path:
107
+ return None
108
+ try:
109
+ with open(event_path, encoding="utf-8") as f:
110
+ return json.load(f).get("repository", {}).get("default_branch")
111
+ except (OSError, ValueError, AttributeError):
112
+ return None
113
+
114
+
115
+ def discover_base_branch(project_dir: str) -> str | None:
116
+ """The branch pull requests should target.
117
+
118
+ Deliberately **not** the checked-out branch: on a pull request the checkout
119
+ is the head branch, and recording that would point every authoring pull
120
+ request at a feature branch that may already be merged.
121
+
122
+ - ``GITHUB_BASE_REF`` / ``CI_MERGE_REQUEST_TARGET_BRANCH_NAME``: set only on
123
+ pull/merge-request runs.
124
+ - ``GITHUB_REF_NAME``: trusted only on a push to the default branch; on other
125
+ pushes, tags, or manual runs it names a feature branch or tag.
126
+ - ``CI_DEFAULT_BRANCH``: GitLab sets the default branch directly.
127
+ - Otherwise, the remote's default branch.
128
+ """
129
+ base_ref = os.environ.get("GITHUB_BASE_REF") or os.environ.get(
130
+ "CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
131
+ )
132
+ if base_ref:
133
+ return base_ref
134
+
135
+ gitlab_default_branch = os.environ.get("CI_DEFAULT_BRANCH")
136
+ if gitlab_default_branch:
137
+ return gitlab_default_branch
138
+
139
+ ref_name = os.environ.get("GITHUB_REF_NAME")
140
+ if (
141
+ ref_name
142
+ and os.environ.get("GITHUB_EVENT_NAME") == "push"
143
+ and os.environ.get("GITHUB_REF_TYPE") == "branch"
144
+ and ref_name == _github_default_branch()
145
+ ):
146
+ return ref_name
147
+
148
+ repo = _open_repo(project_dir)
149
+ if repo is None:
150
+ return None
151
+ from git import RemoteReference, SymbolicReference
152
+
153
+ try:
154
+ target = SymbolicReference(repo, "refs/remotes/origin/HEAD").reference
155
+ except (TypeError, ValueError):
156
+ # No origin/HEAD, or it isn't symbolic.
157
+ return None
158
+ return target.remote_head if isinstance(target, RemoteReference) else None
159
+
160
+
161
+ def discover_repo_root(project_dir: str) -> str | None:
162
+ """The checkout root, which a project directory may sit below.
163
+
164
+ The binding's defs path is repo-relative, and in a workspace the project
165
+ root is a subdirectory of the repo, so the two differ.
166
+ """
167
+ repo = _open_repo(project_dir)
168
+ return str(repo.working_tree_dir) if repo and repo.working_tree_dir else None
@@ -141,13 +141,10 @@ def collect_requirements(code_directory, python_interpreter: str) -> tuple[list[
141
141
  return local_package_paths, deps_lines
142
142
 
143
143
 
144
- # TEMP (protobuf<7 compat): grpcio-health-checking 1.82.0 ships grpc_health/v1/health_pb2
145
- # generated with protobuf-7 gencode, which the protobuf runtime-version guard rejects against
146
- # the protobuf<7 runtime that dagster pins — the code server then crashes on import. The deps
147
- # pex resolves from PyPI, so it pulls 1.82.0 regardless of what the resolved dagster's metadata
148
- # says; force it here so every serverless build is compatible. Remove once dagster's protobuf<7
149
- # cap is lifted.
150
- _EXTRA_BUILD_CONSTRAINTS = ["grpcio-health-checking<1.82"]
144
+ # grpcio-health-checking 1.82.0 ships protobuf-7 gencode but its metadata allows a protobuf 6
145
+ # runtime, so the code server crashes on import when the resolver pairs them. Older dagster
146
+ # releases don't exclude it, so exclude it here for every serverless build.
147
+ _EXTRA_BUILD_CONSTRAINTS = ["grpcio-health-checking!=1.82.0"]
151
148
 
152
149
 
153
150
  def get_deps_requirements(
@@ -10,11 +10,24 @@ from dagster_shared.serdes.serdes import serialize_value
10
10
  from dagster_cloud_cli.core.agent_queue import AgentQueue
11
11
 
12
12
 
13
- @whitelist_for_serdes
13
+ # The repo fields are skipped when unset so deploy data from CLIs that don't
14
+ # report them serializes exactly as before.
15
+ @whitelist_for_serdes(
16
+ skip_when_none_fields={"repo_owner", "repo_name", "base_branch", "defs_base_path", "provider"}
17
+ )
14
18
  @record(kw_only=False)
15
19
  class GitMetadata:
16
20
  commit_hash: str | None = None
17
21
  url: str | None = None
22
+ # Identify the repo, the branch pull requests should target, and the
23
+ # repo-relative folder definitions are autoloaded from. Reported by the
24
+ # deploy so the server knows where UI-authored components belong; all
25
+ # optional, since deploys from older CLIs never send them.
26
+ repo_owner: str | None = None
27
+ repo_name: str | None = None
28
+ base_branch: str | None = None
29
+ defs_base_path: str | None = None
30
+ provider: str | None = None # "github" / "gitlab"
18
31
 
19
32
 
20
33
  @whitelist_for_serdes
@@ -0,0 +1 @@
1
+ __version__ = "1.13.28"
@@ -0,0 +1,179 @@
1
+ """Discovery of the repo and base branch a deploy is running against."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ from dagster_cloud_cli.core import git_repo_context
7
+ from git import Repo
8
+
9
+ _REPOSITORY_ENV_VARS = ("GITHUB_REPOSITORY", "CI_PROJECT_PATH")
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ "remote,expected",
14
+ [
15
+ ("https://github.com/acme/proj", ("acme", "proj")),
16
+ ("https://github.com/acme/proj.git", ("acme", "proj")),
17
+ ("git@github.com:acme/proj.git", ("acme", "proj")),
18
+ ("ssh://git@gitlab.com/acme/proj.git", ("acme", "proj")),
19
+ ("https://gitlab.com/acme/proj/", ("acme", "proj")),
20
+ ("git@gitlab.com:acme/team/proj.git", ("acme/team", "proj")),
21
+ ("https://gitlab.example.com/acme/team/proj.git", ("acme/team", "proj")),
22
+ ("not-a-remote", None),
23
+ ],
24
+ )
25
+ def test_parse_owner_and_repo(remote, expected):
26
+ assert git_repo_context.parse_owner_and_repo(remote) == expected
27
+
28
+
29
+ @pytest.mark.parametrize(
30
+ "env_var,value,expected",
31
+ [
32
+ ("GITHUB_REPOSITORY", "acme/from-ci", ("acme", "from-ci")),
33
+ ("CI_PROJECT_PATH", "acme/team/from-ci", ("acme/team", "from-ci")),
34
+ ],
35
+ )
36
+ def test_prefers_the_ci_provided_repository(monkeypatch, env_var, value, expected):
37
+ # Set by the provider from its own event, so it beats parsing a remote.
38
+ for name in _REPOSITORY_ENV_VARS:
39
+ monkeypatch.delenv(name, raising=False)
40
+ monkeypatch.setenv(env_var, value)
41
+ assert git_repo_context.discover_repo_owner_and_name(".") == expected
42
+
43
+
44
+ def test_reports_nothing_without_a_repository(monkeypatch, tmp_path):
45
+ for name in _REPOSITORY_ENV_VARS:
46
+ monkeypatch.delenv(name, raising=False)
47
+ # tmp_path is not a checkout, so there is no remote to read.
48
+ assert git_repo_context.discover_repo_owner_and_name(str(tmp_path)) == (None, None)
49
+
50
+
51
+ _BRANCH_ENV_VARS = (
52
+ "GITHUB_BASE_REF",
53
+ "GITHUB_REF_NAME",
54
+ "GITHUB_REF_TYPE",
55
+ "GITHUB_EVENT_NAME",
56
+ "GITHUB_EVENT_PATH",
57
+ "CI_MERGE_REQUEST_TARGET_BRANCH_NAME",
58
+ "CI_DEFAULT_BRANCH",
59
+ )
60
+
61
+
62
+ @pytest.fixture
63
+ def github_push(monkeypatch, tmp_path):
64
+ """A GitHub Actions run whose repo's default branch is ``main``."""
65
+ for env_var in _BRANCH_ENV_VARS:
66
+ monkeypatch.delenv(env_var, raising=False)
67
+ event = tmp_path / "event.json"
68
+ event.write_text(json.dumps({"repository": {"default_branch": "main"}}))
69
+ monkeypatch.setenv("GITHUB_EVENT_PATH", str(event))
70
+ monkeypatch.setenv("GITHUB_EVENT_NAME", "push")
71
+ monkeypatch.setenv("GITHUB_REF_TYPE", "branch")
72
+ return monkeypatch
73
+
74
+
75
+ # On a pull request the checkout is the head branch, but the binding wants the
76
+ # branch authoring pull requests should target.
77
+ def test_base_branch_prefers_the_pull_request_base(github_push, tmp_path):
78
+ github_push.setenv("GITHUB_EVENT_NAME", "pull_request")
79
+ github_push.setenv("GITHUB_BASE_REF", "main")
80
+ github_push.setenv("GITHUB_REF_NAME", "feature/some-branch")
81
+ assert git_repo_context.discover_base_branch(str(tmp_path)) == "main"
82
+
83
+
84
+ def test_base_branch_uses_a_push_to_the_default_branch(github_push, tmp_path):
85
+ github_push.setenv("GITHUB_REF_NAME", "main")
86
+ assert git_repo_context.discover_base_branch(str(tmp_path)) == "main"
87
+
88
+
89
+ @pytest.mark.parametrize(
90
+ "event_name,ref_type,ref_name",
91
+ [
92
+ ("push", "branch", "feature/some-branch"),
93
+ ("push", "tag", "v1.2.3"),
94
+ ("workflow_dispatch", "branch", "main"),
95
+ ],
96
+ )
97
+ def test_base_branch_ignores_refs_that_are_not_the_default_branch(
98
+ github_push, tmp_path, event_name, ref_type, ref_name
99
+ ):
100
+ github_push.setenv("GITHUB_EVENT_NAME", event_name)
101
+ github_push.setenv("GITHUB_REF_TYPE", ref_type)
102
+ github_push.setenv("GITHUB_REF_NAME", ref_name)
103
+ # tmp_path is not a checkout, so there is no origin/HEAD to fall back to.
104
+ assert git_repo_context.discover_base_branch(str(tmp_path)) is None
105
+
106
+
107
+ def test_base_branch_prefers_the_merge_request_target(monkeypatch, tmp_path):
108
+ for env_var in _BRANCH_ENV_VARS:
109
+ monkeypatch.delenv(env_var, raising=False)
110
+ monkeypatch.setenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME", "release")
111
+ monkeypatch.setenv("CI_DEFAULT_BRANCH", "main")
112
+ assert git_repo_context.discover_base_branch(str(tmp_path)) == "release"
113
+
114
+
115
+ def test_base_branch_uses_the_gitlab_default_branch(monkeypatch, tmp_path):
116
+ for env_var in _BRANCH_ENV_VARS:
117
+ monkeypatch.delenv(env_var, raising=False)
118
+ monkeypatch.setenv("CI_DEFAULT_BRANCH", "main")
119
+ assert git_repo_context.discover_base_branch(str(tmp_path)) == "main"
120
+
121
+
122
+ def test_base_branch_reports_nothing_outside_a_checkout(monkeypatch, tmp_path):
123
+ for env_var in _BRANCH_ENV_VARS:
124
+ monkeypatch.delenv(env_var, raising=False)
125
+ assert git_repo_context.discover_base_branch(str(tmp_path)) is None
126
+
127
+
128
+ def test_reads_a_checkout(monkeypatch, tmp_path):
129
+ for env_var in (*_REPOSITORY_ENV_VARS, *_BRANCH_ENV_VARS):
130
+ monkeypatch.delenv(env_var, raising=False)
131
+ repo = Repo.init(tmp_path)
132
+ repo.create_remote("origin", "git@github.com:acme/proj.git")
133
+ repo.git.update_ref("refs/remotes/origin/main", repo.index.commit("init").hexsha)
134
+ repo.git.symbolic_ref("refs/remotes/origin/HEAD", "refs/remotes/origin/main")
135
+ # The project may sit below the checkout root.
136
+ project_dir = tmp_path / "proj"
137
+ project_dir.mkdir()
138
+
139
+ assert git_repo_context.discover_repo_owner_and_name(str(project_dir)) == ("acme", "proj")
140
+ assert git_repo_context.discover_base_branch(str(project_dir)) == "main"
141
+
142
+
143
+ _PROVIDER_ENV_VARS = ("GITLAB_CI", "GITHUB_ACTIONS")
144
+
145
+
146
+ @pytest.mark.parametrize(
147
+ "env_var,expected",
148
+ [("GITLAB_CI", "gitlab"), ("GITHUB_ACTIONS", "github")],
149
+ )
150
+ def test_provider_comes_from_the_ci_system(monkeypatch, tmp_path, env_var, expected):
151
+ for name in _PROVIDER_ENV_VARS:
152
+ monkeypatch.delenv(name, raising=False)
153
+ monkeypatch.setenv(env_var, "true")
154
+ assert git_repo_context.discover_git_provider(str(tmp_path)) == expected
155
+
156
+
157
+ # A local deploy has no CI variables, and is often run without --git-url.
158
+ @pytest.mark.parametrize(
159
+ "remote,expected",
160
+ [
161
+ ("git@gitlab.com:acme/team/proj.git", "gitlab"),
162
+ ("https://gitlab.example.com/acme/proj.git", "gitlab"),
163
+ ("git@github.com:acme/proj.git", "github"),
164
+ ("ssh://git@github.com/acme/proj.git", "github"),
165
+ # A self-hosted host that doesn't name its provider can't be told apart.
166
+ ("git@git.example.com:acme/proj.git", None),
167
+ ],
168
+ )
169
+ def test_provider_falls_back_to_the_origin_remote(monkeypatch, tmp_path, remote, expected):
170
+ for name in _PROVIDER_ENV_VARS:
171
+ monkeypatch.delenv(name, raising=False)
172
+ Repo.init(tmp_path).create_remote("origin", remote)
173
+ assert git_repo_context.discover_git_provider(str(tmp_path)) == expected
174
+
175
+
176
+ def test_provider_reports_nothing_outside_a_checkout(monkeypatch, tmp_path):
177
+ for name in _PROVIDER_ENV_VARS:
178
+ monkeypatch.delenv(name, raising=False)
179
+ assert git_repo_context.discover_git_provider(str(tmp_path)) is None
@@ -121,6 +121,13 @@ def test_ci_init_local_branch_deployment(monkeypatch, mocker, empty_config) -> N
121
121
  "build_config": None,
122
122
  "commit_hash": "hash-1234",
123
123
  "git_url": "http://some-git-url",
124
+ # Discovery finds nothing: the project dir is a temp dir, not
125
+ # a checkout, and no CI environment variables are set.
126
+ "git_provider": None,
127
+ "repo_owner": None,
128
+ "repo_name": None,
129
+ "base_branch": None,
130
+ "defs_base_path": None,
124
131
  },
125
132
  "build_output": None,
126
133
  "location_name": "a",
@@ -178,6 +185,13 @@ def test_ci_init(monkeypatch, mocker, empty_config) -> None:
178
185
  "build_config": None,
179
186
  "commit_hash": "hash-1234",
180
187
  "git_url": "http://some-git-url",
188
+ # Discovery finds nothing: the project dir is a temp dir, not
189
+ # a checkout, and no CI environment variables are set.
190
+ "git_provider": None,
191
+ "repo_owner": None,
192
+ "repo_name": None,
193
+ "base_branch": None,
194
+ "defs_base_path": None,
181
195
  },
182
196
  "build_output": None,
183
197
  "location_name": "a",
@@ -353,7 +353,7 @@ test = ["pytest>=6.0.0"]
353
353
 
354
354
  expected_deps = [
355
355
  "dagster-cloud>=1.0.0",
356
- "grpcio-health-checking<1.82",
356
+ "grpcio-health-checking!=1.82.0",
357
357
  "pytest>=6.0.0",
358
358
  "requests>=2.25.0",
359
359
  ]
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dagster-cloud-cli"
7
- version = "1.13.26"
7
+ version = "1.13.28"
8
8
  requires-python = ">=3.10,<3.15"
9
9
  description = ""
10
10
  authors = [{name = "Dagster Labs", email = "hello@dagsterlabs.com"}]
11
11
  license = "Apache-2.0"
12
12
  dependencies = [
13
- "dagster-shared==1.13.26",
13
+ "dagster-shared==1.13.28",
14
14
  "packaging>=20.9",
15
15
  "questionary",
16
16
  "requests",
@@ -18,6 +18,7 @@ dependencies = [
18
18
  "click",
19
19
  "PyYAML>=5.1",
20
20
  "github3.py",
21
+ "gitpython",
21
22
  "Jinja2",
22
23
  "setuptools",
23
24
  "tomli>=1.1.0;python_version<\"3.11\"",
@@ -306,6 +306,7 @@ dependencies = [
306
306
  { name = "click" },
307
307
  { name = "dagster-shared" },
308
308
  { name = "github3-py" },
309
+ { name = "gitpython" },
309
310
  { name = "jinja2" },
310
311
  { name = "packaging" },
311
312
  { name = "pyyaml" },
@@ -333,6 +334,7 @@ requires-dist = [
333
334
  { name = "click" },
334
335
  { name = "dagster-shared", editable = "../dagster-shared" },
335
336
  { name = "github3-py" },
337
+ { name = "gitpython" },
336
338
  { name = "jinja2" },
337
339
  { name = "packaging", specifier = ">=20.9" },
338
340
  { name = "pyyaml", specifier = ">=5.1" },
@@ -408,6 +410,18 @@ wheels = [
408
410
  { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
409
411
  ]
410
412
 
413
+ [[package]]
414
+ name = "gitdb"
415
+ version = "4.0.12"
416
+ source = { registry = "https://pypi.org/simple" }
417
+ dependencies = [
418
+ { name = "smmap" },
419
+ ]
420
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
421
+ wheels = [
422
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
423
+ ]
424
+
411
425
  [[package]]
412
426
  name = "github3-py"
413
427
  version = "4.0.1"
@@ -423,6 +437,18 @@ wheels = [
423
437
  { url = "https://files.pythonhosted.org/packages/61/ad/2394d4fb542574678b0ba342daf734d4d811768da3c2ee0c84d509dcb26c/github3.py-4.0.1-py3-none-any.whl", hash = "sha256:a89af7de25650612d1da2f0609622bcdeb07ee8a45a1c06b2d16a05e4234e753", size = 151800, upload-time = "2023-04-26T17:56:25.015Z" },
424
438
  ]
425
439
 
440
+ [[package]]
441
+ name = "gitpython"
442
+ version = "3.2.0"
443
+ source = { registry = "https://pypi.org/simple" }
444
+ dependencies = [
445
+ { name = "gitdb" },
446
+ ]
447
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/6f6e649818da44d4499604802c89329b8d9799687a124e3a5e467a643336/gitpython-3.2.0.tar.gz", hash = "sha256:fb92310af6844d96adc95ca066ed2e617c00e1dbd146a326626c81e72e18cc2e", size = 238091, upload-time = "2026-09-30T09:10:32.65Z" }
448
+ wheels = [
449
+ { url = "https://files.pythonhosted.org/packages/fa/48/f36d233ce749d1bb880cb5e6f8bb171dc2effabc7435c31da604d831b3ec/gitpython-3.2.0-py3-none-any.whl", hash = "sha256:bd70c5ec05cd2b797423e7eb312147d2458d3cca92085888fba2213f85905537", size = 228537, upload-time = "2026-09-30T09:10:31.159Z" },
450
+ ]
451
+
426
452
  [[package]]
427
453
  name = "idna"
428
454
  version = "3.19"
@@ -968,6 +994,15 @@ wheels = [
968
994
  { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
969
995
  ]
970
996
 
997
+ [[package]]
998
+ name = "smmap"
999
+ version = "5.0.3"
1000
+ source = { registry = "https://pypi.org/simple" }
1001
+ sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
1002
+ wheels = [
1003
+ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
1004
+ ]
1005
+
971
1006
  [[package]]
972
1007
  name = "tomli"
973
1008
  version = "2.4.1"
@@ -1 +0,0 @@
1
- __version__ = "1.13.26"