Skip to content

Commit c4a541f

Browse files
committed
[dg] adds dg plus branch-deployment command
1 parent 02a8ad8 commit c4a541f

11 files changed

Lines changed: 1396 additions & 5 deletions

File tree

python_modules/libraries/dagster-dg-cli/dagster_dg_cli/cli/plus/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import click
22
from dagster_dg_core.utils import DgClickGroup
33

4+
from dagster_dg_cli.cli.plus.branch_deployment import branch_deployment_group
45
from dagster_dg_cli.cli.plus.create import plus_create_group
56
from dagster_dg_cli.cli.plus.deploy import deploy_group
67
from dagster_dg_cli.cli.plus.login import login_command
@@ -11,6 +12,7 @@
1112
name="plus",
1213
cls=DgClickGroup,
1314
commands={
15+
"branch-deployment": branch_deployment_group,
1416
"create": plus_create_group,
1517
"login": login_command,
1618
"pull": plus_pull_group,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Branch deployment command group for Dagster+."""
2+
3+
import click
4+
from dagster_dg_core.utils import DgClickGroup
5+
6+
from dagster_dg_cli.cli.plus.branch_deployment.commands import (
7+
create_or_update_command,
8+
delete_command,
9+
)
10+
11+
12+
@click.group(name="branch-deployment", cls=DgClickGroup)
13+
def branch_deployment_group():
14+
"""Manage branch deployments in Dagster+.
15+
16+
Branch deployments are ephemeral environments that allow you to test code
17+
changes before merging to production. They are automatically created based
18+
on git branches and can include metadata about commits and pull requests.
19+
20+
Use these commands to create, update, and delete branch deployments.
21+
"""
22+
23+
24+
# Register commands
25+
branch_deployment_group.add_command(create_or_update_command)
26+
branch_deployment_group.add_command(delete_command)
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""Commands for managing branch deployments in Dagster+."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
import click
7+
from dagster_cloud_cli.types import SnapshotBaseDeploymentCondition
8+
from dagster_dg_core.shared_options import dg_global_options
9+
from dagster_dg_core.utils import DgClickCommand
10+
from dagster_dg_core.utils.telemetry import cli_telemetry_wrapper
11+
from dagster_shared.plus.config import DagsterPlusCliConfig
12+
13+
from dagster_dg_cli.utils.plus.git_utils import get_git_metadata_for_branch_deployment
14+
from dagster_dg_cli.utils.plus.gql_client import DagsterPlusGraphQLClient
15+
from dagster_dg_cli.utils.plus.gql_mutations import (
16+
create_or_update_branch_deployment,
17+
delete_branch_deployment,
18+
)
19+
20+
21+
def _get_organization(input_organization: Optional[str]) -> str:
22+
"""Get organization from input or config.
23+
24+
Args:
25+
input_organization: Organization from CLI flag (optional)
26+
27+
Returns:
28+
Organization name
29+
30+
Raises:
31+
click.UsageError: If organization not found
32+
"""
33+
organization = input_organization
34+
if not organization:
35+
if not DagsterPlusCliConfig.exists():
36+
raise click.UsageError(
37+
"Organization not specified. To specify an organization, use the --organization option "
38+
"or run `dg plus login`."
39+
)
40+
plus_config = DagsterPlusCliConfig.get()
41+
organization = plus_config.organization
42+
43+
if not organization:
44+
raise click.UsageError(
45+
"Organization not specified. To specify an organization, use the --organization option "
46+
"or run `dg plus login`."
47+
)
48+
return organization
49+
50+
51+
@click.command(name="create-or-update", cls=DgClickCommand)
52+
@click.option(
53+
"--organization",
54+
help="Dagster+ organization name. If not set, defaults to the value set by `dg plus login`.",
55+
envvar="DAGSTER_CLOUD_ORGANIZATION",
56+
)
57+
@click.option(
58+
"--read-git-state",
59+
is_flag=True,
60+
help="Read commit metadata (hash, timestamp, author, message) from git automatically.",
61+
)
62+
@click.option(
63+
"--commit-hash",
64+
help="Git commit hash. Required if --read-git-state is not used.",
65+
)
66+
@click.option(
67+
"--timestamp",
68+
type=float,
69+
help="Commit timestamp in Unix time (seconds since epoch). Required if --read-git-state is not used.",
70+
)
71+
@click.option(
72+
"--commit-message",
73+
help="Commit message for the latest commit.",
74+
)
75+
@click.option(
76+
"--author-name",
77+
help="Author name for the latest commit.",
78+
)
79+
@click.option(
80+
"--author-email",
81+
help="Author email for the latest commit.",
82+
)
83+
@click.option(
84+
"--author-avatar-url",
85+
help="URL for the avatar of the commit author.",
86+
)
87+
@click.option(
88+
"--branch-url",
89+
help="URL to the branch in version control.",
90+
)
91+
@click.option(
92+
"--pull-request-url",
93+
"--code-review-url",
94+
help="URL to the pull request or merge request for this branch.",
95+
)
96+
@click.option(
97+
"--pull-request-status",
98+
help="Status of the pull request (e.g., 'open', 'merged', 'closed').",
99+
)
100+
@click.option(
101+
"--pull-request-number",
102+
"--code-review-id",
103+
help="Pull request or merge request number/ID.",
104+
)
105+
@click.option(
106+
"--base-deployment-name",
107+
help="Name of the deployment to use as the base deployment for comparison.",
108+
)
109+
@click.option(
110+
"--snapshot-base-condition",
111+
type=click.Choice([c.value for c in SnapshotBaseDeploymentCondition]),
112+
help=(
113+
"When to snapshot the base deployment for highlighting changes:\n"
114+
" - on-create: Snapshot when branch deployment is first created\n"
115+
" - on-update: Update snapshot every time branch deployment is updated"
116+
),
117+
)
118+
@dg_global_options
119+
@cli_telemetry_wrapper
120+
def create_or_update_command(
121+
organization: Optional[str],
122+
read_git_state: bool,
123+
commit_hash: Optional[str],
124+
timestamp: Optional[float],
125+
commit_message: Optional[str],
126+
author_name: Optional[str],
127+
author_email: Optional[str],
128+
author_avatar_url: Optional[str],
129+
branch_url: Optional[str],
130+
pull_request_url: Optional[str],
131+
pull_request_status: Optional[str],
132+
pull_request_number: Optional[str],
133+
base_deployment_name: Optional[str],
134+
snapshot_base_condition: Optional[str],
135+
**global_options: object,
136+
) -> None:
137+
r"""Create or update a branch deployment with git metadata.
138+
139+
This command creates or updates a branch deployment in Dagster+ with metadata
140+
about the current git state. Branch deployments are ephemeral environments for
141+
testing code changes before merging to production.
142+
143+
The repository name and branch name are automatically detected from your git
144+
configuration. You can either use --read-git-state to automatically read commit
145+
metadata, or manually specify --commit-hash and --timestamp.
146+
147+
Examples:
148+
# Create/update with auto-detected git metadata
149+
$ dg plus branch-deployment create-or-update --read-git-state
150+
151+
# Create/update with manual commit info
152+
$ dg plus branch-deployment create-or-update \\
153+
--commit-hash abc123def456 \\
154+
--timestamp 1234567890
155+
156+
# Include PR information
157+
$ dg plus branch-deployment create-or-update \\
158+
--read-git-state \\
159+
--pull-request-url https://github.com/org/repo/pull/123 \\
160+
--pull-request-number 123 \\
161+
--pull-request-status open
162+
"""
163+
# Get organization (validates auth is configured)
164+
_get_organization(organization)
165+
166+
# Get git metadata (repo name, branch name, and optionally commit metadata)
167+
_, repo_name, branch_name, git_commit_metadata = get_git_metadata_for_branch_deployment(
168+
Path.cwd(), read_git_state
169+
)
170+
171+
# Validate commit info
172+
if read_git_state and git_commit_metadata:
173+
# Use git metadata
174+
final_commit_hash = str(git_commit_metadata["commit_hash"])
175+
final_timestamp = git_commit_metadata["timestamp"]
176+
# Use git metadata for author info if not explicitly provided
177+
if not commit_message:
178+
commit_message = git_commit_metadata.get("commit_message") # type: ignore
179+
if not author_name:
180+
author_name = git_commit_metadata.get("author_name") # type: ignore
181+
if not author_email:
182+
author_email = git_commit_metadata.get("author_email") # type: ignore
183+
else:
184+
# Use manually provided values
185+
final_commit_hash = commit_hash
186+
final_timestamp = timestamp
187+
188+
# Validate required fields
189+
if not final_commit_hash or final_timestamp is None:
190+
raise click.UsageError(
191+
"Must provide either --read-git-state flag, or both --commit-hash and --timestamp."
192+
)
193+
194+
# Type narrowing - after validation we know these are not None
195+
assert isinstance(final_commit_hash, str)
196+
assert isinstance(final_timestamp, (int, float))
197+
198+
# Convert snapshot_base_condition string to enum
199+
snapshot_enum = None
200+
if snapshot_base_condition:
201+
snapshot_enum = SnapshotBaseDeploymentCondition(snapshot_base_condition)
202+
203+
# Create GraphQL client
204+
plus_config = (
205+
DagsterPlusCliConfig.get() if DagsterPlusCliConfig.exists() else DagsterPlusCliConfig()
206+
)
207+
client = DagsterPlusGraphQLClient.from_config(plus_config)
208+
209+
# Call the mutation
210+
click.echo(f"Creating/updating branch deployment for {repo_name}:{branch_name}...")
211+
deployment_name = create_or_update_branch_deployment(
212+
client=client,
213+
repo_name=repo_name,
214+
branch_name=branch_name,
215+
commit_hash=final_commit_hash,
216+
timestamp=float(final_timestamp),
217+
commit_message=commit_message,
218+
author_name=author_name,
219+
author_email=author_email,
220+
author_avatar_url=author_avatar_url,
221+
branch_url=branch_url,
222+
pull_request_url=pull_request_url,
223+
pull_request_status=pull_request_status,
224+
pull_request_number=pull_request_number,
225+
base_deployment_name=base_deployment_name,
226+
snapshot_base_condition=snapshot_enum,
227+
)
228+
229+
click.echo(f"✓ Branch deployment '{deployment_name}' created/updated successfully")
230+
231+
232+
@click.command(name="delete", cls=DgClickCommand)
233+
@click.argument("deployment", type=str)
234+
@click.option(
235+
"--organization",
236+
help="Dagster+ organization name. If not set, defaults to the value set by `dg plus login`.",
237+
envvar="DAGSTER_CLOUD_ORGANIZATION",
238+
)
239+
@dg_global_options
240+
@cli_telemetry_wrapper
241+
def delete_command(
242+
deployment: str,
243+
organization: Optional[str],
244+
**global_options: object,
245+
) -> None:
246+
"""Delete a branch deployment by name.
247+
248+
This command deletes a branch deployment from Dagster+. Only branch deployments
249+
can be deleted - you cannot delete full deployments or prod deployments with
250+
this command.
251+
252+
Arguments:
253+
DEPLOYMENT: Name of the branch deployment to delete
254+
255+
Examples:
256+
# Delete a branch deployment
257+
$ dg plus branch-deployment delete my-feature-branch
258+
259+
# Delete with explicit organization
260+
$ dg plus branch-deployment delete my-feature-branch --organization myorg
261+
"""
262+
# Get organization (this also validates we have auth configured)
263+
_get_organization(organization)
264+
265+
# Create GraphQL client
266+
plus_config = (
267+
DagsterPlusCliConfig.get() if DagsterPlusCliConfig.exists() else DagsterPlusCliConfig()
268+
)
269+
client = DagsterPlusGraphQLClient.from_config(plus_config)
270+
271+
# Delete the deployment
272+
click.echo(f"Deleting branch deployment '{deployment}'...")
273+
deployment_id = delete_branch_deployment(client, deployment)
274+
275+
click.echo(f"✓ Branch deployment '{deployment}' (ID: {deployment_id}) deleted successfully")

0 commit comments

Comments
 (0)