Skip to content

Commit f7c1e5f

Browse files
committed
fix!: check for base branch when deploying to prod
1 parent d5c3ca7 commit f7c1e5f

File tree

7 files changed

+219
-22
lines changed

7 files changed

+219
-22
lines changed

docs/integrations/github.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ In this example we configured the merge method to be `squash`. See [Bot Configur
113113

114114
One way to signal to SQLMesh that a PR is ready to go to production is through the use of "Required Approvers".
115115
In this approach users configure their SQLMesh project to list users that are designated as "Required Approver" and then when the bot detects an approval was received from one of these individuals then it determines that it is time to deploy to production.
116+
The bot will only do the deploy to prod if the base branch is a production branch (as defined in the bot's configuration but defaults to either `main` or `master`).
116117
This pattern can be a great fit for teams that already have an approval process like this in place and therefore it actually removes an extra step from either the author or the approver since SQLMesh will automate the deployment and merge until of it having to be manually done.
117118

118119
##### Required Approval Configuration
@@ -285,18 +286,19 @@ Below is an example of how to define the default config for the bot in either YA
285286

286287
### Configuration Properties
287288

288-
| Option | Description | Type | Required |
289-
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:|
290-
| `invalidate_environment_after_deploy` | Indicates if the PR environment created should be automatically invalidated after changes are deployed. Invalidated environments are cleaned up automatically by the Janitor. Default: `True` | bool | N |
291-
| `merge_method` | The merge method to use when automatically merging a PR after deploying to prod. Defaults to `None` meaning automatic merge is not done. Options: `merge`, `squash`, `rebase` | string | N |
292-
| `enable_deploy_command` | Indicates if the `/deploy` command should be enabled in order to allowed synchronized deploys to production. Default: `False` | bool | N |
293-
| `command_namespace` | The namespace to use for SQLMesh commands. For example if you provide `#SQLMesh` as a value then commands will be expected in the format of `#SQLMesh/<command>`. Default: `None` meaning no namespace is used. | string | N |
294-
| `auto_categorize_changes` | Auto categorization behavior to use for the bot. If not provided then the project-wide categorization behavior is used. See [Auto-categorize model changes](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#auto-categorize-model-changes) for details. | dict | N |
295-
| `default_pr_start` | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `None` meaning the start date is set to the earliest model's start or to 1 day ago if [data previews](../concepts/plans.md#data-preview) need to be computed. | str | N |
296-
| `skip_pr_backfill` | Indicates if the bot should skip backfilling models in the PR environment. Default: `True` | bool | N |
297-
| `pr_include_unmodified` | Indicates whether to include unmodified models in the PR environment. Default to the project's config value (which defaults to `False`) | bool | N |
298-
| `run_on_deploy_to_prod` | Indicates whether to run latest intervals when deploying to prod. If set to false, the deployment will backfill only the changed models up to the existing latest interval in production, ignoring any missing intervals beyond this point. Default: `False` | bool | N |
299-
| `pr_environment_name` | The name of the PR environment to create for which a PR number will be appended to. Defaults to the repo name if not provided. Note: The name will be normalized to alphanumeric + underscore and lowercase. | str | N |
289+
| Option | Description | Type | Required |
290+
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------:|:--------:|
291+
| `invalidate_environment_after_deploy` | Indicates if the PR environment created should be automatically invalidated after changes are deployed. Invalidated environments are cleaned up automatically by the Janitor. Default: `True` | bool | N |
292+
| `merge_method` | The merge method to use when automatically merging a PR after deploying to prod. Defaults to `None` meaning automatic merge is not done. Options: `merge`, `squash`, `rebase` | string | N |
293+
| `enable_deploy_command` | Indicates if the `/deploy` command should be enabled in order to allowed synchronized deploys to production. Default: `False` | bool | N |
294+
| `command_namespace` | The namespace to use for SQLMesh commands. For example if you provide `#SQLMesh` as a value then commands will be expected in the format of `#SQLMesh/<command>`. Default: `None` meaning no namespace is used. | string | N |
295+
| `auto_categorize_changes` | Auto categorization behavior to use for the bot. If not provided then the project-wide categorization behavior is used. See [Auto-categorize model changes](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#auto-categorize-model-changes) for details. | dict | N |
296+
| `default_pr_start` | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `None` meaning the start date is set to the earliest model's start or to 1 day ago if [data previews](../concepts/plans.md#data-preview) need to be computed. | str | N |
297+
| `skip_pr_backfill` | Indicates if the bot should skip backfilling models in the PR environment. Default: `True` | bool | N |
298+
| `pr_include_unmodified` | Indicates whether to include unmodified models in the PR environment. Default to the project's config value (which defaults to `False`) | bool | N |
299+
| `run_on_deploy_to_prod` | Indicates whether to run latest intervals when deploying to prod. If set to false, the deployment will backfill only the changed models up to the existing latest interval in production, ignoring any missing intervals beyond this point. Default: `False` | bool | N |
300+
| `pr_environment_name` | The name of the PR environment to create for which a PR number will be appended to. Defaults to the repo name if not provided. Note: The name will be normalized to alphanumeric + underscore and lowercase. | str | N |
301+
| `prod_branch_names` | The name of the branch name(s) associated with production. Typically will be a single branch but is defined as a list. Ex: ["prod"]. Default: ["main", "master"] | list[str] | N |
300302

301303
Example with all properties defined:
302304

@@ -317,6 +319,8 @@ Example with all properties defined:
317319
default_pr_start: "1 week ago"
318320
skip_pr_backfill: false
319321
run_on_deploy_to_prod: false
322+
prod_branch_names:
323+
- production
320324
```
321325

322326
=== "Python"
@@ -340,6 +344,7 @@ Example with all properties defined:
340344
default_pr_start="1 week ago",
341345
skip_pr_backfill=False,
342346
run_on_deploy_to_prod=False,
347+
prod_branch_names=["production"],
343348
)
344349
)
345350
```

sqlmesh/integrations/github/cicd/command.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _run_all(controller: GithubController) -> None:
210210
has_required_approval = False
211211
is_auto_deploying_prod = (
212212
controller.deploy_command_enabled or controller.do_required_approval_check
213-
)
213+
) and controller.is_branched_from_prod
214214
if controller.is_comment_added:
215215
if not controller.deploy_command_enabled:
216216
# We aren't using commands so we can just return
@@ -267,7 +267,7 @@ def _run_all(controller: GithubController) -> None:
267267
status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.SKIPPED
268268
)
269269
deployed_to_prod = False
270-
if has_required_approval and prod_plan_generated:
270+
if has_required_approval and prod_plan_generated and controller.is_branched_from_prod:
271271
deployed_to_prod = _deploy_production(controller)
272272
elif is_auto_deploying_prod:
273273
if not has_required_approval:
@@ -292,7 +292,7 @@ def _run_all(controller: GithubController) -> None:
292292
if (
293293
not pr_environment_updated
294294
or not prod_plan_generated
295-
or (has_required_approval and not deployed_to_prod)
295+
or (has_required_approval and controller.is_branched_from_prod and not deployed_to_prod)
296296
):
297297
raise CICDBotError(
298298
"A step of the run-all check failed. See check status for more information."

sqlmesh/integrations/github/cicd/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class GithubCICDBotConfig(BaseConfig):
2828
pr_include_unmodified: t.Optional[bool] = None
2929
run_on_deploy_to_prod: bool = False
3030
pr_environment_name: t.Optional[str] = None
31+
prod_branch_names: t.List[str] = ["main", "master"]
3132

3233
@model_validator(mode="before")
3334
@classmethod

sqlmesh/integrations/github/cicd/controller.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def create_from_pull_request_url(cls, pull_request_url: str) -> PullRequestInfo:
7676
return cls(
7777
owner=owner,
7878
repo=repo,
79-
pr_number=pr_number,
79+
pr_number=int(pr_number),
8080
)
8181

8282

@@ -444,6 +444,10 @@ def modified_snapshots(self) -> t.Dict[SnapshotId, t.Union[Snapshot, SnapshotTab
444444
def removed_snapshots(self) -> t.Set[SnapshotId]:
445445
return set(self.prod_plan_with_gaps.context_diff.removed_snapshots)
446446

447+
@property
448+
def is_branched_from_prod(self) -> bool:
449+
return self._pull_request.base.ref in self.bot_config.prod_branch_names
450+
447451
@classmethod
448452
def _append_output(cls, key: str, value: str) -> None:
449453
"""

tests/integrations/github/cicd/fixtures.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ def github_client(mocker: MockerFixture):
2525

2626
client_mock = mocker.MagicMock(spec=Github)
2727
mocker.patch("github.Github", client_mock)
28+
2829
mock_repository = mocker.MagicMock(spec=Repository)
30+
client_mock.get_repo.return_value = mock_repository
31+
2932
mock_pull_request = mocker.MagicMock(spec=PullRequest)
30-
mock_pull_request.get_reviews = mocker.MagicMock(
31-
side_effect=[mocker.MagicMock(spec=PullRequestReview)]
32-
)
33-
mock_repository.get_pull = mocker.MagicMock(side_effect=mock_pull_request)
34-
mock_repository.get_issue = mocker.MagicMock(side_effect=mocker.MagicMock(spec=Issue))
35-
client_mock.get_repo = mocker.MagicMock(side_effect=mock_repository)
33+
mock_pull_request.base.ref = "main"
34+
mock_pull_request.get_reviews.return_value = [mocker.MagicMock(spec=PullRequestReview)]
35+
mock_repository.get_pull.return_value = mock_pull_request
36+
mock_repository.get_issue.return_value = mocker.MagicMock(spec=Issue)
3637

3738
return client_mock
3839

tests/integrations/github/cicd/test_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_load_yaml_config_default(tmp_path):
4040
assert config.cicd_bot.skip_pr_backfill
4141
assert config.cicd_bot.pr_include_unmodified is None
4242
assert config.cicd_bot.pr_environment_name is None
43+
assert config.cicd_bot.prod_branch_names == ["main", "master"]
4344

4445

4546
def test_load_yaml_config(tmp_path):
@@ -62,6 +63,8 @@ def test_load_yaml_config(tmp_path):
6263
skip_pr_backfill: false
6364
pr_include_unmodified: true
6465
pr_environment_name: "MyOverride"
66+
prod_branch_names:
67+
- testing
6568
model_defaults:
6669
dialect: duckdb
6770
""",
@@ -85,6 +88,7 @@ def test_load_yaml_config(tmp_path):
8588
assert not config.cicd_bot.skip_pr_backfill
8689
assert config.cicd_bot.pr_include_unmodified
8790
assert config.cicd_bot.pr_environment_name == "MyOverride"
91+
assert config.cicd_bot.prod_branch_names == ["testing"]
8892

8993

9094
def test_load_python_config_defaults(tmp_path):
@@ -115,6 +119,7 @@ def test_load_python_config_defaults(tmp_path):
115119
assert config.cicd_bot.skip_pr_backfill
116120
assert config.cicd_bot.pr_include_unmodified is None
117121
assert config.cicd_bot.pr_environment_name is None
122+
assert config.cicd_bot.prod_branch_names == ["main", "master"]
118123

119124

120125
def test_load_python_config(tmp_path):
@@ -141,6 +146,7 @@ def test_load_python_config(tmp_path):
141146
skip_pr_backfill=False,
142147
pr_include_unmodified=True,
143148
pr_environment_name="MyOverride",
149+
prod_branch_names=["testing"],
144150
),
145151
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
146152
)
@@ -166,6 +172,7 @@ def test_load_python_config(tmp_path):
166172
assert not config.cicd_bot.skip_pr_backfill
167173
assert config.cicd_bot.pr_include_unmodified
168174
assert config.cicd_bot.pr_environment_name == "MyOverride"
175+
assert config.cicd_bot.prod_branch_names == ["testing"]
169176

170177

171178
def test_validation(tmp_path):

0 commit comments

Comments
 (0)