Skip to content

feat: add snow run command for project scripts#2823

Open
sfc-gh-mborins wants to merge 7 commits intomainfrom
snow-run
Open

feat: add snow run command for project scripts#2823
sfc-gh-mborins wants to merge 7 commits intomainfrom
snow-run

Conversation

@sfc-gh-mborins
Copy link

Pre-review checklist

  • I've confirmed that instructions included in README.md are still correct after my changes in the codebase.
  • I've added or updated automated unit tests to verify correctness of my new code.
  • I've added or updated integration tests to verify correctness of my new code.
  • I've confirmed that my changes are working by executing CLI's commands manually on MacOS.
  • I've confirmed that my changes are working by executing CLI's commands manually on Windows.
  • I've confirmed that my changes are up-to-date with the target branch.
  • I've described my changes in the release notes.
  • I've described my changes in the section below.
  • I've described my changes in the documentation.

Changes description

Add a new snow run command that allows users to define and execute project-specific scripts in snowflake.yml or manifest.yml. Features include:

  • Load script from either snowflake.yml or manifest.yml with conflict detection
  • Track script source file and show in --list output
  • Script definitions with cmd, description, shell, cwd, and env options
  • Composite scripts using run: to sequence multiple scripts
  • Variable interpolation with ${env.VAR} syntax
  • CLI overrides with -D flag
  • Dry-run mode and continue-on-error for composite scripts
  • Extensive unit and integration tests covering various scenarios and edge cases

@sfc-gh-mborins sfc-gh-mborins requested a review from a team as a code owner March 18, 2026 18:13
Add a new `snow run` command that allows users to define and execute
project-specific scripts in snowflake.yml or manifest.yml. Features include:

- Load script from either snowflake.yml or manifest.yml with conflict detection
- Track script source file and show in --list output
- Script definitions with cmd, description, shell, cwd, and env options
- Composite scripts using `run:` to sequence multiple scripts
- Variable interpolation with ${env.VAR} syntax
- CLI overrides with -D flag
- Dry-run mode and continue-on-error for composite scripts
- Extensive unit and integration tests covering various scenarios and edge cases

.... Generated with [Cortex Code](https://docs.snowflake.com/user-guide/snowflake-cortex/cortex-agents)

Co-Authored-By: Cortex Code <[email protected]>
Fix code quality issues (trailing whitespace, ruff G004 f-string logging,
black formatting), Windows test failures (subprocess mock was polluting
global scope via shared module reference), and update E2E snapshots to
include the new run command in snow --help output.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <[email protected]>
sfc-gh-mborins and others added 2 commits March 18, 2026 18:36
Prevents infinite recursion when scripts reference each other by tracking
the call stack during composite script execution.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <[email protected]>
- Use shlex.quote() on interpolated variable values when shell=True
  to prevent command injection via shell metacharacters
- Track and return the first non-zero exit code from composite scripts
  instead of always returning hardcoded exit code 1

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <[email protected]>
sfc-gh-mborins and others added 2 commits March 18, 2026 18:58
… preservation

- test_run_shell_mode_escapes_interpolated_variables: verifies shlex.quote
  on interpolated values prevents command injection in shell mode
- test_run_extra_args_with_spaces_are_quoted: verifies extra args with
  spaces are properly quoted
- test_run_composite_preserves_first_failure_exit_code: verifies composite
  scripts return the actual first failure exit code

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <[email protected]>

manager = ScriptManager(project_root)

if list_scripts:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work with --format=json?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to use CollectionResult/ObjectResult to have support for different formatting options. I don't think json/csv would be very useful in this case, but having some response in those formats is better than nothing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another issue I see is that introduces an inconsistency with other cli commands. As raised before, we usually have a dedicated command for listing objects

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with a simple script like

scripts:
  show_pwd:
    cmd: "pwd"
    description: "run pwd"

we get this:

snow run show_pwd --format json
/Users/jwilkowski/snowflake/dcm_test/dcm_manifest_v2
{
    "message": ""
}

Comment on lines +125 to +126
for v in parse_key_value_variables(var_overrides):
vars_dict[v.key] = v.value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for v in parse_key_value_variables(var_overrides):
vars_dict[v.key] = v.value
vars_dict = {v.key: v.value for v in parse_key_value_variables(var_overrides)}

More pythonic

Copy link
Contributor

@sfc-gh-jwilkowski sfc-gh-jwilkowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to request some changes. I think that some design decisions require more discussion with @sfc-gh-jbilek (like --list as flag rather than standalone command)

if not script:
available = list(manager.list_scripts().keys())
if available:
raise ClickException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please raise CliError

Comment on lines +69 to +75
raise ClickException(
"Scripts defined in both manifest.yml and snowflake.yml.\n"
"Scripts must be defined in only one file per directory.\n\n"
"Recommendation: Move all scripts to one file.\n"
"- Use manifest.yml for DCM-focused projects\n"
"- Use snowflake.yml for app-focused projects"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud - is that a bad thing really? Maybe we can merge scripts from both sources?

for name, script in scripts.items():
desc = script.description or "(no description)"
cc.message(f" {name:20} {desc}")
return MessageResult("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use EmptyResult


manager = ScriptManager(project_root)

if list_scripts:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to use CollectionResult/ObjectResult to have support for different formatting options. I don't think json/csv would be very useful in this case, but having some response in those formats is better than nothing


manager = ScriptManager(project_root)

if list_scripts:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another issue I see is that introduces an inconsistency with other cli commands. As raised before, we usually have a dedicated command for listing objects

Comment on lines +113 to +121
if scripts:
source = manager.scripts_source or "project"
cc.message(f"Available scripts from {source} (use --list for details):")
for name in scripts:
cc.message(f" {name}")
return MessageResult("\nSpecify a script name to run, or use --list")
return MessageResult(
"No scripts defined. Add a 'scripts' section to snowflake.yml or manifest.yml"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can first deal with simple case to reduce indentation levels

Suggested change
if scripts:
source = manager.scripts_source or "project"
cc.message(f"Available scripts from {source} (use --list for details):")
for name in scripts:
cc.message(f" {name}")
return MessageResult("\nSpecify a script name to run, or use --list")
return MessageResult(
"No scripts defined. Add a 'scripts' section to snowflake.yml or manifest.yml"
)
if not scripts:
return MessageResult(
"No scripts defined. Add a 'scripts' section to snowflake.yml or manifest.yml"
)
source = manager.scripts_source or "project"
cc.message(f"Available scripts from {source} (use --list for details):")
for name in scripts:
cc.message(f" {name}")
return MessageResult("\nSpecify a script name to run, or use --list")

Comment on lines +148 to +149
if not result.success:
raise typer.Exit(result.exit_code)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit biased here, as on one hand I'd prefer this minimalistic way of reporting failures, but on the other hand we're not telling what failed or why. Also it's not consistent with how we report errors

Comment on lines +122 to +127
for name, script_def in scripts_data.items():
if isinstance(script_def, str):
result[name] = ScriptModel(cmd=script_def)
else:
result[name] = ScriptModel(**script_def)
return result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No error handling for malformed manifest.yaml. We've recently added DCMManifest class and afaik that's the only manifest we have right now, so might be better to use that or even extract some base fields such as manifest_version, project_type and scripts to a base class

Comment on lines +212 to +213
log.warning("Variable '${%s}' not found, leaving as-is", var_name)
return match.group(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather raise an error here, as using the var name could lead to dangerous outcomes.


log = logging.getLogger(__name__)

VARIABLE_PATTERN = re.compile(r"\$\{([^}]+)\}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about introducing another syntax for variable interpolation. Our users might get confused with those different syntax versions living side by side https://github.com/snowflakedb/snowflake-cli-templates/blob/main/streamlit_vnext_multi_page/snowflake.yml#L6-L9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants