From ab0a5c2e8da0a94a6d5c73c220b95a5a6bd7cdd7 Mon Sep 17 00:00:00 2001 From: Michel El Nacouzi Date: Tue, 7 Jan 2025 17:15:03 -0500 Subject: [PATCH] Add --from-stage option to version create --- RELEASE-NOTES.md | 1 + .../nativeapp/entities/application_package.py | 36 +++++--- .../_plugins/nativeapp/version/commands.py | 7 ++ .../cli/_plugins/workspace/commands.py | 1 + tests/__snapshots__/test_help_messages.ambr | 7 ++ tests/nativeapp/test_version_create.py | 63 ++++++++++++++ tests_integration/nativeapp/test_version.py | 82 +++++++++++++++++++ 7 files changed, 185 insertions(+), 12 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f02f22e273..d7c281b787 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -25,6 +25,7 @@ * `snow app release-directive set` * `snow app release-directive unset` * `snow app version create` now returns version, patch, and label in JSON format. +* Add `--from-stage` flag to `snow app version create` to allow version creation from the content of the stage without re-syncing to the stage. * Add support for release channels: * Add support for release channels feature in native app version creation/drop. * Add ability to specify release channel when creating application instance from release directive: `snow app run --from-release-directive --channel=` diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 1da13ca9d5..058a5377d1 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -441,6 +441,7 @@ def action_version_create( skip_git_check: bool, interactive: bool, force: bool, + from_stage: Optional[bool], *args, **kwargs, ) -> VersionInfo: @@ -476,18 +477,29 @@ def action_version_create( if git_policy.should_proceed(): self.check_index_changes_in_git_repo(policy=policy, interactive=interactive) - self._deploy( - action_ctx=action_ctx, - bundle_map=bundle_map, - prune=True, - recursive=True, - paths=[], - print_diff=True, - validate=True, - stage_fqn=self.stage_fqn, - interactive=interactive, - force=force, - ) + # if user is asking to create the version from the current stage, + # then do not re-deploy the artifacts or touch the stage + if from_stage: + # verify package exists: + show_obj_row = self.get_existing_app_pkg_info() + if not show_obj_row: + raise ClickException( + "Cannot create version from stage because the application package does not exist yet. " + "Try removing --from-stage flag or executing `snow app deploy` to deploy the application package first." + ) + else: + self._deploy( + action_ctx=action_ctx, + bundle_map=bundle_map, + prune=True, + recursive=True, + paths=[], + print_diff=True, + validate=True, + stage_fqn=self.stage_fqn, + interactive=interactive, + force=force, + ) # Warn if the version exists in a release directive(s) try: diff --git a/src/snowflake/cli/_plugins/nativeapp/version/commands.py b/src/snowflake/cli/_plugins/nativeapp/version/commands.py index b7ad13c0c0..67ead08136 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/commands.py @@ -72,6 +72,12 @@ def create( help="When enabled, the Snowflake CLI skips checking if your project has any untracked or stages files in git. Default: unset.", is_flag=True, ), + from_stage: bool = typer.Option( + False, + "--from-stage", + help="When enabled, the Snowflake CLI creates a version from the current application package stage without syncing to the stage first.", + is_flag=True, + ), interactive: bool = InteractiveOption, force: Optional[bool] = ForceOption, **options, @@ -95,6 +101,7 @@ def create( force=force, interactive=interactive, skip_git_check=skip_git_check, + from_stage=from_stage, ) message = "Version create is now complete." diff --git a/src/snowflake/cli/_plugins/workspace/commands.py b/src/snowflake/cli/_plugins/workspace/commands.py index e09078df45..b8f9a331bb 100644 --- a/src/snowflake/cli/_plugins/workspace/commands.py +++ b/src/snowflake/cli/_plugins/workspace/commands.py @@ -293,6 +293,7 @@ def version_create( skip_git_check=skip_git_check, interactive=interactive, force=force, + from_stage=False, ) diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 9e8d9b808d..6776c1e7eb 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -1923,6 +1923,13 @@ | untracked or stages | | files in git. Default: | | unset. | + | --from-stage When enabled, the | + | Snowflake CLI creates | + | a version from the | + | current application | + | package stage without | + | syncing to the stage | + | first. | | --interactive --no-interactive When enabled, this | | option displays | | prompts even if the | diff --git a/tests/nativeapp/test_version_create.py b/tests/nativeapp/test_version_create.py index fe70c0d6d6..72c6d0c9c6 100644 --- a/tests/nativeapp/test_version_create.py +++ b/tests/nativeapp/test_version_create.py @@ -42,10 +42,12 @@ from tests.nativeapp.factories import ApplicationPackageEntityModelFactory, PdfV2Factory from tests.nativeapp.utils import ( + APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, APPLICATION_PACKAGE_ENTITY_MODULE, SQL_EXECUTOR_EXECUTE, SQL_FACADE, SQL_FACADE_CREATE_VERSION, + SQL_FACADE_SHOW_RELEASE_DIRECTIVES, mock_execute_helper, mock_snowflake_yml_file_v2, ) @@ -64,6 +66,7 @@ def _version_create( skip_git_check: bool, label: str | None = None, console: AbstractConsole | None = None, + from_stage: bool = False, ): dm = DefinitionManager() pd = dm.project_definition @@ -83,6 +86,7 @@ def _version_create( force=force, interactive=interactive, skip_git_check=skip_git_check, + from_stage=from_stage, ) @@ -931,3 +935,62 @@ def test_patch_from_manifest( mock_console.warning.assert_called_with( f"Cannot resolve version. Found patch: {manifest_patch} in manifest.yml which is different from provided patch {cli_patch}." ) + + +@mock.patch(SQL_FACADE_CREATE_VERSION) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES, return_value=[]) +@mock.patch.object(ApplicationPackageEntity, "_deploy") +@mock.patch( + APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, return_value=[{"name": "app_pkg"}] +) +@mock.patch.object( + ApplicationPackageEntity, "check_index_changes_in_git_repo", return_value=None +) +@mock.patch.object( + ApplicationPackageEntity, "get_existing_version_info", return_value=None +) +@mock.patch.object(ApplicationPackageEntity, "_bundle") +def test_action_version_create_from_stage( + mock_bundle, + mock_get_existing_version_info, + mock_check_git, + mock_get_existing_pkg_info, + mock_deploy, + mock_show_release_directives, + mock_create_version, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + version = "v1" + result = application_package_entity.action_version_create( + action_ctx=action_context, + version=version, + patch=None, + label=None, + skip_git_check=False, + interactive=False, + force=False, + from_stage=True, + ) + + assert result == VersionInfo(version, 0, None) + + mock_check_git.assert_called_once() + mock_show_release_directives.assert_called_once_with( + package_name=pkg_model.fqn.name, role=pkg_model.meta.role + ) + mock_get_existing_version_info.assert_called_once_with(version) + mock_bundle.assert_called_once() + mock_create_version.assert_called_once_with( + package_name=pkg_model.fqn.name, + version=version, + stage_fqn=application_package_entity.stage_fqn, + role=pkg_model.meta.role, + label=None, + ) + + # Deploy should not be called with --from-stage + mock_deploy.assert_not_called() diff --git a/tests_integration/nativeapp/test_version.py b/tests_integration/nativeapp/test_version.py index 3214457102..d2725ee5fa 100644 --- a/tests_integration/nativeapp/test_version.py +++ b/tests_integration/nativeapp/test_version.py @@ -637,3 +637,85 @@ def test_version_create_with_json_result(runner, nativeapp_project_directory): "label": None, "message": "Version create is now complete.", } + + +@pytest.mark.integration +def test_version_from_stage(runner, nativeapp_project_directory): + with nativeapp_project_directory("napp_init_v2"): + # Deploy: + result = runner.invoke_with_connection_json(["app", "deploy"]) + assert result.exit_code == 0 + + # Add a file and make sure it shows up in the diff against the stage: + with open("app/TEST_UPDATE.md", "w") as f: + f.write("Hello world!") + result = runner.invoke_with_connection(["app", "diff"]) + assert result.exit_code == 0 + assert "TEST_UPDATE.md" in result.output + + # Test version creation: + result = runner.invoke_with_connection_json( + [ + "app", + "version", + "create", + "v1", + "--force", + "--skip-git-check", + "--from-stage", + ] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 0, + "label": None, + "message": "Version create is now complete.", + } + + # Make sure the file still hasn't been deployed yet: + result = runner.invoke_with_connection(["app", "diff"]) + assert result.exit_code == 0 + assert "TEST_UPDATE.md" in result.output + + # Create patch: + result = runner.invoke_with_connection_json( + [ + "app", + "version", + "create", + "v1", + "--force", + "--skip-git-check", + "--from-stage", + ] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 1, + "label": None, + "message": "Version create is now complete.", + } + + # Make sure the file still hasn't been deployed yet: + result = runner.invoke_with_connection(["app", "diff"]) + assert result.exit_code == 0 + assert "TEST_UPDATE.md" in result.output + + # Create patch but don't use --from-stage: + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1", "--force", "--skip-git-check"] + ) + assert result.exit_code == 0 + assert result.json == { + "version": "v1", + "patch": 2, + "label": None, + "message": "Version create is now complete.", + } + + # Make sure the file has been actually deployed: + result = runner.invoke_with_connection(["app", "diff"]) + assert result.exit_code == 0 + assert "TEST_UPDATE.md" not in result.output