Skip to content

Commit f0e4b6c

Browse files
feat: [SNOW-1942865] wip: integrate cicd into project init
1 parent daa4f78 commit f0e4b6c

File tree

5 files changed

+172
-3
lines changed

5 files changed

+172
-3
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) 2025 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import enum
2+
from typing import Generator, List, Optional, Type
3+
4+
from snowflake.cli.api.secure_path import SecurePath
5+
6+
7+
class CIProviderChoices(str, enum.Enum):
8+
GITHUB = "GITHUB"
9+
GITLAB = "GITLAB"
10+
11+
12+
class CIProvider:
13+
name: str
14+
files_to_render_directories: list[str] = []
15+
16+
@classmethod
17+
def cleanup(cls, root: SecurePath) -> None:
18+
raise NotImplementedError()
19+
20+
def __eq__(self, other) -> bool:
21+
return self.name == other.name
22+
23+
@classmethod
24+
def from_choice(cls, choice: CIProviderChoices) -> "CIProvider":
25+
return {
26+
CIProviderChoices.GITHUB: GithubProvider,
27+
CIProviderChoices.GITLAB: GitLabProvider,
28+
}[choice]()
29+
30+
@classmethod
31+
def all(cls) -> List[Type["CIProvider"]]: # noqa: A003
32+
return [GithubProvider, GitLabProvider]
33+
34+
def get_files_to_render(self, root_dir: SecurePath) -> Generator[str, None, None]:
35+
for directory in self.files_to_render_directories:
36+
for path in (root_dir / directory).rglob("*"):
37+
yield str(path.relative_to(root_dir.path))
38+
39+
def has_template(self, root_dir: SecurePath) -> bool:
40+
raise NotImplementedError()
41+
42+
43+
class GithubProvider(CIProvider):
44+
name = CIProviderChoices.GITLAB.name
45+
files_to_render_directories = [".github/workflows/"]
46+
47+
@classmethod
48+
def cleanup(cls, root_dir: SecurePath):
49+
(root_dir / ".github").rmdir(recursive=True)
50+
51+
def has_template(self, root_dir: SecurePath) -> bool:
52+
return (root_dir / ".github/workflows").exists()
53+
54+
55+
class GitLabProvider(CIProvider):
56+
name = CIProviderChoices.GITLAB.name
57+
58+
@classmethod
59+
def cleanup(cls, root_dir: SecurePath):
60+
(root_dir / ".gitlab-ci.yml").unlink(missing_ok=True)
61+
62+
def get_files_to_render(self, root_dir: SecurePath):
63+
if (root_dir / ".gitlab-ci.yml").exists():
64+
return [".gitlab-ci.yml"]
65+
66+
def has_template(self, root_dir: SecurePath) -> bool:
67+
return (root_dir / ".gitlab-ci.yml").exists()
68+
69+
70+
class CIProviderManager:
71+
@staticmethod
72+
def project_post_gen_cleanup(
73+
selected_provider: Optional[CIProvider], template_root: SecurePath
74+
):
75+
for provider_cls in CIProvider.all():
76+
if selected_provider and not isinstance(selected_provider, provider_cls):
77+
provider_cls.cleanup(template_root)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# from snowflake.cli._plugins.cicd import commands
16+
17+
#
18+
# @plugin_hook_impl
19+
# def command_spec():
20+
# return CommandSpec(
21+
# parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
22+
# command_type=CommandType.COMMAND_GROUP,
23+
# typer_instance=commands.app.create_instance(),
24+
# )

src/snowflake/cli/_plugins/init/commands.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
import yaml
2222
from click import ClickException
2323
from snowflake.cli.__about__ import VERSION
24+
from snowflake.cli._plugins.cicd.manager import (
25+
CIProvider,
26+
CIProviderChoices,
27+
CIProviderManager,
28+
)
2429
from snowflake.cli.api.commands.flags import (
2530
NoInteractiveOption,
2631
variables_option,
@@ -42,6 +47,7 @@
4247

4348

4449
DEFAULT_SOURCE = "https://github.com/snowflakedb/snowflake-cli-templates"
50+
DEFAULT_CI_SOURCE = DEFAULT_SOURCE + "/cicd"
4551

4652
log = logging.getLogger(__name__)
4753

@@ -72,6 +78,17 @@ def _path_argument_callback(path: str) -> str:
7278
"--template-source",
7379
help=f"local path to template directory or URL to git repository with templates.",
7480
)
81+
CIProviderOption = typer.Option(
82+
None,
83+
"--ci-provider",
84+
help=f"CI provider to generate workflow for.",
85+
case_sensitive=True,
86+
)
87+
CITemplateSourceOption = typer.Option(
88+
None,
89+
"--ci-template-source",
90+
help=f"local path to template directory or URL to git repository with ci/cd templates.",
91+
)
7592
VariablesOption = variables_option(
7693
"String in `key=value` format. Provided variables will not be prompted for."
7794
)
@@ -191,6 +208,8 @@ def init(
191208
path: str = PathArgument,
192209
template: Optional[str] = TemplateOption,
193210
template_source: Optional[str] = SourceOption,
211+
ci_provider: Optional[CIProviderChoices] = CIProviderOption,
212+
ci_template_source: Optional[str] = CITemplateSourceOption,
194213
variables: Optional[List[str]] = VariablesOption,
195214
no_interactive: bool = NoInteractiveOption,
196215
**options,
@@ -219,6 +238,28 @@ def init(
219238
destination=tmpdir,
220239
)
221240

241+
if ci_provider:
242+
ci_provider_instance = CIProvider.from_choice(ci_provider)
243+
if ci_template_source is not None:
244+
with SecurePath.temporary_directory() as cicd_tmpdir:
245+
cicd_template_root = _fetch_remote_template(
246+
url=ci_template_source,
247+
path=None,
248+
destination=cicd_tmpdir
249+
# type: ignore
250+
)
251+
cicd_template_root.copy(template_root.path)
252+
253+
elif ci_provider_instance.has_template(template_root):
254+
pass # template has ci files
255+
else:
256+
# use generic ci/cd template
257+
with SecurePath.temporary_directory() as cicd_tmpdir:
258+
cicd_template_root = _fetch_remote_template(
259+
url=DEFAULT_SOURCE, path=f"cicd/{ci_provider_instance.name.lower()}", destination=cicd_tmpdir # type: ignore
260+
)
261+
cicd_template_root.copy(template_root.path)
262+
222263
template_metadata = _read_template_metadata(
223264
template_root, args_error_msg=args_error_msg
224265
)
@@ -233,16 +274,22 @@ def init(
233274
"project_dir_name": SecurePath(path).name,
234275
"snowflake_cli_version": VERSION,
235276
}
236-
log.debug(
237-
"Rendering template files: %s", ", ".join(template_metadata.files_to_render)
277+
files_to_render = template_metadata.files_to_render + list(
278+
ci_provider_instance.get_files_to_render(template_root)
238279
)
280+
log.debug("Rendering template files: %s", ", ".join(files_to_render))
239281
render_template_files(
240282
template_root=template_root,
241-
files_to_render=template_metadata.files_to_render,
283+
files_to_render=files_to_render,
242284
data=variable_values,
243285
)
244286
_remove_template_metadata_file(template_root)
287+
post_generate(template_root, ci_provider_instance)
245288
SecurePath(path).parent.mkdir(exist_ok=True, parents=True)
246289
template_root.copy(path)
247290

248291
return MessageResult(f"Initialized the new project in {path}")
292+
293+
294+
def post_generate(template_root: SecurePath, ci_provider: Optional[CIProvider]):
295+
CIProviderManager.project_post_gen_cleanup(ci_provider, template_root)

src/snowflake/cli/api/secure_path.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ def glob(self, pattern: str):
116116
"""
117117
return self._path.glob(pattern)
118118

119+
def rglob(self, pattern: str) -> Generator[Path, None, None]:
120+
"""
121+
Recursively yield all existing files (of any kind, including
122+
directories) matching the given relative pattern, anywhere in
123+
this subtree.
124+
"""
125+
return self._path.rglob(pattern)
126+
119127
def as_posix(self) -> str:
120128
"""
121129
Return the string representation of the path with forward slashes (/) as the path separator.

0 commit comments

Comments
 (0)