mcp-souschef 2.2.0__py3-none-any.whl → 2.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.8.0.dist-info}/METADATA +226 -38
- mcp_souschef-2.8.0.dist-info/RECORD +42 -0
- mcp_souschef-2.8.0.dist-info/entry_points.txt +4 -0
- souschef/__init__.py +10 -2
- souschef/assessment.py +113 -30
- souschef/ci/__init__.py +11 -0
- souschef/ci/github_actions.py +379 -0
- souschef/ci/gitlab_ci.py +299 -0
- souschef/ci/jenkins_pipeline.py +343 -0
- souschef/cli.py +605 -5
- souschef/converters/__init__.py +2 -2
- souschef/converters/cookbook_specific.py +125 -0
- souschef/converters/cookbook_specific.py.backup +109 -0
- souschef/converters/playbook.py +853 -15
- souschef/converters/resource.py +103 -1
- souschef/core/constants.py +13 -0
- souschef/core/path_utils.py +12 -9
- souschef/core/validation.py +35 -2
- souschef/deployment.py +29 -27
- souschef/filesystem/operations.py +0 -7
- souschef/parsers/__init__.py +6 -1
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/inspec.py +343 -18
- souschef/parsers/metadata.py +30 -0
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +429 -178
- souschef/ui/__init__.py +8 -0
- souschef/ui/app.py +2998 -0
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +497 -0
- souschef/ui/pages/cookbook_analysis.py +1360 -0
- mcp_souschef-2.2.0.dist-info/RECORD +0 -31
- mcp_souschef-2.2.0.dist-info/entry_points.txt +0 -4
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.8.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.8.0.dist-info}/licenses/LICENSE +0 -0
souschef/ci/gitlab_ci.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""GitLab CI generation from Chef CI/CD patterns."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_gitlab_ci_from_chef_ci(
|
|
10
|
+
cookbook_path: str,
|
|
11
|
+
project_name: str,
|
|
12
|
+
enable_cache: bool = True,
|
|
13
|
+
enable_artifacts: bool = True,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Generate .gitlab-ci.yml from Chef cookbook CI/CD patterns.
|
|
17
|
+
|
|
18
|
+
Analyzes Chef testing tools and generates equivalent GitLab CI stages.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
cookbook_path: Path to Chef cookbook.
|
|
22
|
+
project_name: GitLab project name.
|
|
23
|
+
enable_cache: Enable caching for dependencies.
|
|
24
|
+
enable_artifacts: Enable artifacts for test results.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
GitLab CI YAML content.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
# Analyse Chef CI patterns
|
|
31
|
+
ci_patterns = _analyse_chef_ci_patterns(cookbook_path)
|
|
32
|
+
|
|
33
|
+
# Generate CI configuration
|
|
34
|
+
return _generate_gitlab_ci_yaml(
|
|
35
|
+
project_name, ci_patterns, enable_cache, enable_artifacts
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _analyse_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Analyse Chef cookbook for CI/CD patterns.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
cookbook_path: Path to Chef cookbook.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary of detected CI patterns.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
base_path = Path(cookbook_path)
|
|
51
|
+
|
|
52
|
+
patterns: dict[str, Any] = {
|
|
53
|
+
"has_kitchen": (base_path / ".kitchen.yml").exists(),
|
|
54
|
+
"has_chefspec": (base_path / "spec").exists(),
|
|
55
|
+
"has_inspec": (base_path / "test" / "integration").exists(),
|
|
56
|
+
"has_berksfile": (base_path / "Berksfile").exists(),
|
|
57
|
+
"lint_tools": [],
|
|
58
|
+
"test_suites": [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Detect linting tools
|
|
62
|
+
lint_tools: list[str] = patterns["lint_tools"]
|
|
63
|
+
if (base_path / ".foodcritic").exists():
|
|
64
|
+
lint_tools.append("foodcritic")
|
|
65
|
+
if (base_path / ".cookstyle.yml").exists():
|
|
66
|
+
lint_tools.append("cookstyle")
|
|
67
|
+
|
|
68
|
+
# Parse kitchen.yml for test suites
|
|
69
|
+
kitchen_file = base_path / ".kitchen.yml"
|
|
70
|
+
if kitchen_file.exists():
|
|
71
|
+
try:
|
|
72
|
+
test_suites: list[str] = patterns["test_suites"]
|
|
73
|
+
with kitchen_file.open() as f:
|
|
74
|
+
kitchen_config = yaml.safe_load(f)
|
|
75
|
+
if kitchen_config and "suites" in kitchen_config:
|
|
76
|
+
test_suites.extend(
|
|
77
|
+
suite["name"] for suite in kitchen_config["suites"]
|
|
78
|
+
)
|
|
79
|
+
except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
|
|
80
|
+
# Gracefully handle malformed .kitchen.yml - continue with empty
|
|
81
|
+
# test suites. Catches: YAML syntax errors, file I/O errors,
|
|
82
|
+
# missing config keys, type mismatches
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
return patterns
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_lint_jobs(ci_patterns: dict[str, Any], enable_artifacts: bool) -> list[str]:
|
|
89
|
+
"""Build lint job configurations."""
|
|
90
|
+
jobs = []
|
|
91
|
+
if ci_patterns.get("lint_tools"):
|
|
92
|
+
lint_commands = []
|
|
93
|
+
for tool in ci_patterns["lint_tools"]:
|
|
94
|
+
if tool == "cookstyle":
|
|
95
|
+
lint_commands.append(" - ansible-lint playbooks/")
|
|
96
|
+
elif tool == "foodcritic":
|
|
97
|
+
lint_commands.append(" - yamllint -c .yamllint .")
|
|
98
|
+
|
|
99
|
+
if lint_commands:
|
|
100
|
+
jobs.append(
|
|
101
|
+
_create_gitlab_job(
|
|
102
|
+
"lint:ansible",
|
|
103
|
+
"lint",
|
|
104
|
+
lint_commands,
|
|
105
|
+
allow_failure=False,
|
|
106
|
+
artifacts=enable_artifacts,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
return jobs
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_test_jobs(ci_patterns: dict[str, Any], enable_artifacts: bool) -> list[str]:
|
|
113
|
+
"""Build test job configurations."""
|
|
114
|
+
jobs = []
|
|
115
|
+
|
|
116
|
+
# Unit test job (ChefSpec → Molecule)
|
|
117
|
+
if ci_patterns.get("has_chefspec"):
|
|
118
|
+
jobs.append(
|
|
119
|
+
_create_gitlab_job(
|
|
120
|
+
"test:unit",
|
|
121
|
+
"test",
|
|
122
|
+
[" - molecule test --scenario-name default"],
|
|
123
|
+
artifacts=enable_artifacts,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Integration test jobs (Kitchen → Molecule)
|
|
128
|
+
if ci_patterns.get("has_kitchen") or ci_patterns.get("has_inspec"):
|
|
129
|
+
if ci_patterns.get("test_suites"):
|
|
130
|
+
for suite in ci_patterns["test_suites"]:
|
|
131
|
+
jobs.append(
|
|
132
|
+
_create_gitlab_job(
|
|
133
|
+
f"test:integration:{suite}",
|
|
134
|
+
"test",
|
|
135
|
+
[f" - molecule test --scenario-name {suite}"],
|
|
136
|
+
artifacts=enable_artifacts,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
jobs.append(
|
|
141
|
+
_create_gitlab_job(
|
|
142
|
+
"test:integration",
|
|
143
|
+
"test",
|
|
144
|
+
[" - molecule test"],
|
|
145
|
+
artifacts=enable_artifacts,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return jobs
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _generate_gitlab_ci_yaml(
|
|
153
|
+
project_name: str,
|
|
154
|
+
ci_patterns: dict[str, Any],
|
|
155
|
+
enable_cache: bool,
|
|
156
|
+
enable_artifacts: bool,
|
|
157
|
+
) -> str:
|
|
158
|
+
"""
|
|
159
|
+
Generate GitLab CI YAML configuration.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
project_name: Project name.
|
|
163
|
+
ci_patterns: Detected CI patterns.
|
|
164
|
+
enable_cache: Enable caching.
|
|
165
|
+
enable_artifacts: Enable artifacts.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
GitLab CI YAML content.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
stages = ["lint", "test", "deploy"]
|
|
172
|
+
jobs = []
|
|
173
|
+
|
|
174
|
+
# Global configuration
|
|
175
|
+
config_lines = [
|
|
176
|
+
f"# .gitlab-ci.yml: {project_name}",
|
|
177
|
+
"# Generated from Chef cookbook CI/CD patterns",
|
|
178
|
+
"",
|
|
179
|
+
"image: python:3.11",
|
|
180
|
+
"",
|
|
181
|
+
"stages:",
|
|
182
|
+
]
|
|
183
|
+
for stage in stages:
|
|
184
|
+
config_lines.append(f" - {stage}")
|
|
185
|
+
config_lines.append("")
|
|
186
|
+
|
|
187
|
+
# Cache configuration
|
|
188
|
+
if enable_cache:
|
|
189
|
+
config_lines.extend(
|
|
190
|
+
[
|
|
191
|
+
"cache:",
|
|
192
|
+
" paths:",
|
|
193
|
+
" - .cache/pip",
|
|
194
|
+
" - venv/",
|
|
195
|
+
"",
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Variables
|
|
200
|
+
config_lines.extend(
|
|
201
|
+
[
|
|
202
|
+
"variables:",
|
|
203
|
+
" PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip",
|
|
204
|
+
" ANSIBLE_FORCE_COLOR: 'true'",
|
|
205
|
+
"",
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Before script
|
|
210
|
+
config_lines.extend(
|
|
211
|
+
[
|
|
212
|
+
"before_script:",
|
|
213
|
+
" - python -m venv venv",
|
|
214
|
+
" - source venv/bin/activate",
|
|
215
|
+
" - pip install --upgrade pip",
|
|
216
|
+
" - pip install ansible ansible-lint molecule molecule-docker",
|
|
217
|
+
"",
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Build jobs
|
|
222
|
+
jobs.extend(_build_lint_jobs(ci_patterns, enable_artifacts))
|
|
223
|
+
jobs.extend(_build_test_jobs(ci_patterns, enable_artifacts))
|
|
224
|
+
|
|
225
|
+
# Deploy job
|
|
226
|
+
jobs.append(
|
|
227
|
+
_create_gitlab_job(
|
|
228
|
+
"deploy:production",
|
|
229
|
+
"deploy",
|
|
230
|
+
[
|
|
231
|
+
(
|
|
232
|
+
" - ansible-playbook -i inventory/production "
|
|
233
|
+
"playbooks/site.yml --check"
|
|
234
|
+
),
|
|
235
|
+
" - ansible-playbook -i inventory/production playbooks/site.yml",
|
|
236
|
+
],
|
|
237
|
+
when="manual",
|
|
238
|
+
only_branches=["main", "master"],
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Combine configuration and jobs
|
|
243
|
+
config = "\n".join(config_lines) + "\n" + "\n".join(jobs)
|
|
244
|
+
return config
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _create_gitlab_job(
|
|
248
|
+
name: str,
|
|
249
|
+
stage: str,
|
|
250
|
+
script: list[str],
|
|
251
|
+
allow_failure: bool = False,
|
|
252
|
+
artifacts: bool = False,
|
|
253
|
+
when: str | None = None,
|
|
254
|
+
only_branches: list[str] | None = None,
|
|
255
|
+
) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Create a GitLab CI job.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
name: Job name.
|
|
261
|
+
stage: Stage name.
|
|
262
|
+
script: List of script commands.
|
|
263
|
+
allow_failure: Allow job failure.
|
|
264
|
+
artifacts: Enable artifacts.
|
|
265
|
+
when: When to run (manual, on_success, etc).
|
|
266
|
+
only_branches: Only run on specific branches.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
GitLab CI job YAML block.
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
job_lines = [f"{name}:", f" stage: {stage}", " script:"]
|
|
273
|
+
job_lines.extend(script)
|
|
274
|
+
|
|
275
|
+
if allow_failure:
|
|
276
|
+
job_lines.append(" allow_failure: true")
|
|
277
|
+
|
|
278
|
+
if artifacts:
|
|
279
|
+
job_lines.extend(
|
|
280
|
+
[
|
|
281
|
+
" artifacts:",
|
|
282
|
+
" reports:",
|
|
283
|
+
" junit: test-results/*.xml",
|
|
284
|
+
" paths:",
|
|
285
|
+
" - test-results/",
|
|
286
|
+
" expire_in: 1 week",
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if when:
|
|
291
|
+
job_lines.append(f" when: {when}")
|
|
292
|
+
|
|
293
|
+
if only_branches:
|
|
294
|
+
job_lines.append(" only:")
|
|
295
|
+
for branch in only_branches:
|
|
296
|
+
job_lines.append(f" - {branch}")
|
|
297
|
+
|
|
298
|
+
job_lines.append("")
|
|
299
|
+
return "\n".join(job_lines)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Jenkins pipeline generation from Chef CI/CD patterns."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_jenkinsfile_from_chef_ci(
|
|
10
|
+
cookbook_path: str,
|
|
11
|
+
pipeline_name: str,
|
|
12
|
+
pipeline_type: str = "declarative",
|
|
13
|
+
enable_parallel: bool = True,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Generate Jenkinsfile from Chef cookbook CI/CD patterns.
|
|
17
|
+
|
|
18
|
+
Analyzes Chef testing tools (kitchen-ci, foodcritic, cookstyle, chefspec)
|
|
19
|
+
and generates equivalent Jenkins pipeline stages.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cookbook_path: Path to Chef cookbook.
|
|
23
|
+
pipeline_name: Name for the Jenkins pipeline.
|
|
24
|
+
pipeline_type: 'declarative' or 'scripted'.
|
|
25
|
+
enable_parallel: Enable parallel execution of test stages.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Jenkinsfile content (Groovy DSL).
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
# Analyse Chef CI patterns
|
|
32
|
+
ci_patterns = _analyse_chef_ci_patterns(cookbook_path)
|
|
33
|
+
|
|
34
|
+
if pipeline_type == "declarative":
|
|
35
|
+
return _generate_declarative_pipeline(
|
|
36
|
+
pipeline_name, ci_patterns, enable_parallel
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
return _generate_scripted_pipeline(pipeline_name, enable_parallel)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _analyse_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Analyse Chef cookbook for CI/CD patterns.
|
|
45
|
+
|
|
46
|
+
Detects:
|
|
47
|
+
- Test Kitchen configuration (.kitchen.yml)
|
|
48
|
+
- ChefSpec tests (spec/)
|
|
49
|
+
- InSpec tests (test/integration/)
|
|
50
|
+
- Foodcritic/Cookstyle linting
|
|
51
|
+
- Berksfile dependencies
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cookbook_path: Path to Chef cookbook.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary of detected CI patterns.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
base_path = Path(cookbook_path)
|
|
61
|
+
|
|
62
|
+
patterns: dict[str, Any] = {
|
|
63
|
+
"has_kitchen": (base_path / ".kitchen.yml").exists(),
|
|
64
|
+
"has_chefspec": (base_path / "spec").exists(),
|
|
65
|
+
"has_inspec": (base_path / "test" / "integration").exists(),
|
|
66
|
+
"has_berksfile": (base_path / "Berksfile").exists(),
|
|
67
|
+
"lint_tools": [],
|
|
68
|
+
"test_suites": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Detect linting tools
|
|
72
|
+
lint_tools: list[str] = patterns["lint_tools"]
|
|
73
|
+
if (base_path / ".foodcritic").exists():
|
|
74
|
+
lint_tools.append("foodcritic")
|
|
75
|
+
if (base_path / ".cookstyle.yml").exists():
|
|
76
|
+
lint_tools.append("cookstyle")
|
|
77
|
+
|
|
78
|
+
# Parse kitchen.yml for test suites
|
|
79
|
+
kitchen_file = base_path / ".kitchen.yml"
|
|
80
|
+
if kitchen_file.exists():
|
|
81
|
+
try:
|
|
82
|
+
test_suites: list[str] = patterns["test_suites"]
|
|
83
|
+
with kitchen_file.open() as f:
|
|
84
|
+
kitchen_config = yaml.safe_load(f)
|
|
85
|
+
if kitchen_config and "suites" in kitchen_config:
|
|
86
|
+
test_suites.extend(
|
|
87
|
+
suite["name"] for suite in kitchen_config["suites"]
|
|
88
|
+
)
|
|
89
|
+
except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
|
|
90
|
+
# Gracefully handle malformed .kitchen.yml - continue with empty config
|
|
91
|
+
# Catches: YAML syntax errors, file I/O errors, missing config keys,
|
|
92
|
+
# type mismatches in config structure, and missing dict attributes
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
return patterns
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _create_lint_stage(ci_patterns: dict[str, Any]) -> str | None:
|
|
99
|
+
"""Create lint stage if lint tools are detected."""
|
|
100
|
+
if not ci_patterns.get("lint_tools"):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
lint_steps: list[str] = []
|
|
104
|
+
for tool in ci_patterns["lint_tools"]:
|
|
105
|
+
if tool == "cookstyle":
|
|
106
|
+
lint_steps.append("sh 'ansible-lint playbooks/'")
|
|
107
|
+
elif tool == "foodcritic":
|
|
108
|
+
lint_steps.append("sh 'yamllint -c .yamllint .'")
|
|
109
|
+
|
|
110
|
+
if not lint_steps:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
return _create_stage("Lint", lint_steps)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _create_unit_test_stage(ci_patterns: dict[str, Any]) -> str | None:
|
|
117
|
+
"""Create unit test stage if ChefSpec is detected."""
|
|
118
|
+
if not ci_patterns.get("has_chefspec"):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
return _create_stage(
|
|
122
|
+
"Unit Tests",
|
|
123
|
+
["sh 'molecule test --scenario-name default'"],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _create_integration_test_stage(ci_patterns: dict[str, Any]) -> str | None:
|
|
128
|
+
"""Create integration test stage if Test Kitchen or InSpec is detected."""
|
|
129
|
+
if not (ci_patterns.get("has_kitchen") or ci_patterns.get("has_inspec")):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
test_steps = []
|
|
133
|
+
if ci_patterns.get("test_suites"):
|
|
134
|
+
for suite in ci_patterns["test_suites"]:
|
|
135
|
+
test_steps.append(f"sh 'molecule test --scenario-name {suite}'")
|
|
136
|
+
else:
|
|
137
|
+
test_steps.append("sh 'molecule test'")
|
|
138
|
+
|
|
139
|
+
return _create_stage("Integration Tests", test_steps)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _create_deploy_stage() -> str:
|
|
143
|
+
"""Create deploy stage."""
|
|
144
|
+
return _create_stage(
|
|
145
|
+
"Deploy",
|
|
146
|
+
[
|
|
147
|
+
(
|
|
148
|
+
"sh 'ansible-playbook -i inventory/production "
|
|
149
|
+
"playbooks/site.yml --check'"
|
|
150
|
+
),
|
|
151
|
+
"input message: 'Deploy to production?', ok: 'Deploy'",
|
|
152
|
+
"sh 'ansible-playbook -i inventory/production playbooks/site.yml'",
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _generate_declarative_pipeline(
|
|
158
|
+
pipeline_name: str, ci_patterns: dict[str, Any], enable_parallel: bool = True
|
|
159
|
+
) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Generate Jenkins Declarative Pipeline.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
pipeline_name: Pipeline name.
|
|
165
|
+
ci_patterns: Detected CI patterns.
|
|
166
|
+
enable_parallel: Enable parallel execution of test stages.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Jenkinsfile with Declarative Pipeline syntax.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
stages = []
|
|
173
|
+
|
|
174
|
+
# Collect test stages for potential parallel execution
|
|
175
|
+
test_stages = []
|
|
176
|
+
|
|
177
|
+
lint_stage = _create_lint_stage(ci_patterns)
|
|
178
|
+
if lint_stage:
|
|
179
|
+
test_stages.append(lint_stage)
|
|
180
|
+
|
|
181
|
+
unit_stage = _create_unit_test_stage(ci_patterns)
|
|
182
|
+
if unit_stage:
|
|
183
|
+
test_stages.append(unit_stage)
|
|
184
|
+
|
|
185
|
+
integration_stage = _create_integration_test_stage(ci_patterns)
|
|
186
|
+
if integration_stage:
|
|
187
|
+
test_stages.append(integration_stage)
|
|
188
|
+
|
|
189
|
+
# Add test stages (parallel or sequential based on enable_parallel)
|
|
190
|
+
if enable_parallel and len(test_stages) > 1:
|
|
191
|
+
# Wrap multiple test stages in parallel block
|
|
192
|
+
parallel_content = "\n".join(test_stages)
|
|
193
|
+
parallel_stage = f"""stage('Test') {{
|
|
194
|
+
parallel {{
|
|
195
|
+
{_indent_content(parallel_content, 16)}
|
|
196
|
+
}}
|
|
197
|
+
}}"""
|
|
198
|
+
stages.append(parallel_stage)
|
|
199
|
+
else:
|
|
200
|
+
# Execute stages sequentially
|
|
201
|
+
stages.extend(test_stages)
|
|
202
|
+
|
|
203
|
+
# Always add deploy stage (never parallelized)
|
|
204
|
+
stages.append(_create_deploy_stage())
|
|
205
|
+
|
|
206
|
+
# Build pipeline
|
|
207
|
+
stages_groovy = "\n\n".join(stages)
|
|
208
|
+
|
|
209
|
+
return f"""// Jenkinsfile: {pipeline_name}
|
|
210
|
+
// Generated from Chef cookbook CI/CD patterns
|
|
211
|
+
// Pipeline Type: Declarative
|
|
212
|
+
|
|
213
|
+
pipeline {{
|
|
214
|
+
agent any
|
|
215
|
+
|
|
216
|
+
options {{
|
|
217
|
+
timestamps()
|
|
218
|
+
ansiColor('xterm')
|
|
219
|
+
buildDiscarder(logRotator(numToKeepStr: '10'))
|
|
220
|
+
}}
|
|
221
|
+
|
|
222
|
+
environment {{
|
|
223
|
+
ANSIBLE_FORCE_COLOR = 'true'
|
|
224
|
+
ANSIBLE_HOST_KEY_CHECKING = 'false'
|
|
225
|
+
}}
|
|
226
|
+
|
|
227
|
+
stages {{
|
|
228
|
+
{_indent_content(stages_groovy, 8)}
|
|
229
|
+
}}
|
|
230
|
+
|
|
231
|
+
post {{
|
|
232
|
+
always {{
|
|
233
|
+
cleanWs()
|
|
234
|
+
}}
|
|
235
|
+
success {{
|
|
236
|
+
echo 'Pipeline succeeded!'
|
|
237
|
+
}}
|
|
238
|
+
failure {{
|
|
239
|
+
echo 'Pipeline failed!'
|
|
240
|
+
}}
|
|
241
|
+
}}
|
|
242
|
+
}}
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _generate_scripted_pipeline(
|
|
247
|
+
pipeline_name: str, enable_parallel: bool = True
|
|
248
|
+
) -> str:
|
|
249
|
+
"""
|
|
250
|
+
Generate Jenkins Scripted Pipeline.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
pipeline_name: Pipeline name.
|
|
254
|
+
enable_parallel: Enable parallel execution of test stages.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Jenkinsfile with Scripted Pipeline syntax.
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
if enable_parallel:
|
|
261
|
+
test_block = """ parallel(
|
|
262
|
+
lint: {{
|
|
263
|
+
stage('Lint') {{
|
|
264
|
+
sh 'ansible-lint playbooks/'
|
|
265
|
+
}}
|
|
266
|
+
}},
|
|
267
|
+
test: {{
|
|
268
|
+
stage('Test') {{
|
|
269
|
+
sh 'molecule test'
|
|
270
|
+
}}
|
|
271
|
+
}}
|
|
272
|
+
)"""
|
|
273
|
+
else:
|
|
274
|
+
test_block = """ stage('Lint') {{
|
|
275
|
+
sh 'ansible-lint playbooks/'
|
|
276
|
+
}}
|
|
277
|
+
|
|
278
|
+
stage('Test') {{
|
|
279
|
+
sh 'molecule test'
|
|
280
|
+
}}"""
|
|
281
|
+
|
|
282
|
+
return f"""// Jenkinsfile: {pipeline_name}
|
|
283
|
+
// Generated from Chef cookbook CI/CD patterns
|
|
284
|
+
// Pipeline Type: Scripted
|
|
285
|
+
|
|
286
|
+
node {{
|
|
287
|
+
try {{
|
|
288
|
+
stage('Checkout') {{
|
|
289
|
+
checkout scm
|
|
290
|
+
}}
|
|
291
|
+
|
|
292
|
+
{test_block}
|
|
293
|
+
|
|
294
|
+
stage('Deploy') {{
|
|
295
|
+
input message: 'Deploy to production?', ok: 'Deploy'
|
|
296
|
+
sh 'ansible-playbook -i inventory/production playbooks/site.yml'
|
|
297
|
+
}}
|
|
298
|
+
}} catch (Exception e) {{
|
|
299
|
+
currentBuild.result = 'FAILURE'
|
|
300
|
+
throw e
|
|
301
|
+
}} finally {{
|
|
302
|
+
cleanWs()
|
|
303
|
+
}}
|
|
304
|
+
}}
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _create_stage(name: str, steps: list[str]) -> str:
|
|
309
|
+
"""
|
|
310
|
+
Create a Jenkins Declarative Pipeline stage.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
name: Stage name.
|
|
314
|
+
steps: List of steps (shell commands or Jenkins DSL).
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Groovy stage block.
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
steps_formatted = "\n".join(f" {step}" for step in steps)
|
|
321
|
+
return f"""stage('{name}') {{
|
|
322
|
+
steps {{
|
|
323
|
+
{steps_formatted}
|
|
324
|
+
}}
|
|
325
|
+
}}"""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _indent_content(content: str, spaces: int) -> str:
|
|
329
|
+
"""
|
|
330
|
+
Indent multi-line content.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
content: Content to indent.
|
|
334
|
+
spaces: Number of spaces to indent.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Indented content.
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
indent = " " * spaces
|
|
341
|
+
return "\n".join(
|
|
342
|
+
indent + line if line.strip() else line for line in content.split("\n")
|
|
343
|
+
)
|