mcp-souschef 2.2.0__py3-none-any.whl → 2.5.3__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.5.3.dist-info}/METADATA +174 -21
- mcp_souschef-2.5.3.dist-info/RECORD +38 -0
- mcp_souschef-2.5.3.dist-info/entry_points.txt +4 -0
- souschef/assessment.py +100 -17
- 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 +601 -1
- souschef/core/validation.py +35 -2
- souschef/deployment.py +5 -3
- souschef/filesystem/operations.py +0 -7
- souschef/parsers/__init__.py +6 -1
- souschef/parsers/inspec.py +343 -18
- souschef/parsers/metadata.py +30 -0
- souschef/server.py +394 -141
- souschef/ui/__init__.py +8 -0
- souschef/ui/app.py +1837 -0
- souschef/ui/pages/cookbook_analysis.py +425 -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.5.3.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Actions workflow generation from Chef CI/CD patterns.
|
|
3
|
+
|
|
4
|
+
Analyzes Chef testing tools (Test Kitchen, ChefSpec, Cookstyle) and
|
|
5
|
+
generates equivalent GitHub Actions workflows with proper job
|
|
6
|
+
configuration and caching.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
# GitHub Actions constants
|
|
15
|
+
ACTION_CHECKOUT = "actions/checkout@v4"
|
|
16
|
+
ACTION_SETUP_RUBY = "ruby/setup-ruby@v1"
|
|
17
|
+
ACTION_CACHE = "actions/cache@v4"
|
|
18
|
+
ACTION_UPLOAD_ARTIFACT = "actions/upload-artifact@v4"
|
|
19
|
+
|
|
20
|
+
STEP_NAME_CHECKOUT = "Checkout code"
|
|
21
|
+
STEP_NAME_SETUP_RUBY = "Setup Ruby"
|
|
22
|
+
STEP_NAME_CACHE_GEMS = "Cache gems"
|
|
23
|
+
STEP_NAME_INSTALL_DEPS = "Install dependencies"
|
|
24
|
+
|
|
25
|
+
GEM_BUNDLE_PATH = "vendor/bundle"
|
|
26
|
+
GEM_CACHE_KEY = "gems-${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}"
|
|
27
|
+
GEM_CACHE_RESTORE_KEY = "gems-${{ runner.os }}-"
|
|
28
|
+
|
|
29
|
+
BUNDLE_INSTALL_CMD = "bundle install --jobs 4 --retry 3"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_github_workflow_from_chef_ci(
|
|
33
|
+
cookbook_path: str,
|
|
34
|
+
workflow_name: str = "Chef Cookbook CI",
|
|
35
|
+
enable_cache: bool = True,
|
|
36
|
+
enable_artifacts: bool = True,
|
|
37
|
+
) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Generate GitHub Actions workflow from Chef cookbook CI/CD patterns.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
cookbook_path: Path to Chef cookbook directory.
|
|
43
|
+
workflow_name: Name for the GitHub Actions workflow.
|
|
44
|
+
enable_cache: Enable caching for Chef dependencies.
|
|
45
|
+
enable_artifacts: Enable artifacts for test results.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
GitHub Actions workflow YAML content.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
cookbook_dir = Path(cookbook_path)
|
|
52
|
+
if not cookbook_dir.exists():
|
|
53
|
+
raise FileNotFoundError(f"Cookbook directory not found: {cookbook_path}")
|
|
54
|
+
|
|
55
|
+
# Analyze Chef CI patterns
|
|
56
|
+
patterns = _analyze_chef_ci_patterns(cookbook_dir)
|
|
57
|
+
|
|
58
|
+
# Build workflow structure
|
|
59
|
+
workflow = _build_workflow_structure(
|
|
60
|
+
workflow_name, patterns, enable_cache, enable_artifacts
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return yaml.dump(workflow, default_flow_style=False, sort_keys=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _analyze_chef_ci_patterns(cookbook_dir: Path) -> dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Analyze Chef cookbook for CI/CD patterns and testing configurations.
|
|
69
|
+
|
|
70
|
+
This function examines a Chef cookbook directory to detect various
|
|
71
|
+
testing and linting tools, as well as Test Kitchen configurations
|
|
72
|
+
including suites and platforms.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
cookbook_dir: Path to the Chef cookbook directory to analyze.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary containing detected patterns with the following keys:
|
|
79
|
+
- has_kitchen (bool): Whether Test Kitchen is configured
|
|
80
|
+
(.kitchen.yml exists)
|
|
81
|
+
- has_chefspec (bool): Whether ChefSpec tests are present
|
|
82
|
+
(spec/**/*_spec.rb files)
|
|
83
|
+
- has_cookstyle (bool): Whether Cookstyle is configured
|
|
84
|
+
(.cookstyle.yml exists)
|
|
85
|
+
- has_foodcritic (bool): Whether Foodcritic (legacy) is
|
|
86
|
+
configured (.foodcritic exists)
|
|
87
|
+
- kitchen_suites (list[str]): Names of Test Kitchen suites
|
|
88
|
+
found in .kitchen.yml
|
|
89
|
+
- kitchen_platforms (list[str]): Names of Test Kitchen
|
|
90
|
+
platforms found in .kitchen.yml
|
|
91
|
+
|
|
92
|
+
Note:
|
|
93
|
+
If .kitchen.yml is malformed or cannot be parsed, the function
|
|
94
|
+
continues with empty suite and platform lists rather than
|
|
95
|
+
raising an exception.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> patterns = _analyze_chef_ci_patterns(Path("/path/to/cookbook"))
|
|
99
|
+
>>> patterns["has_kitchen"]
|
|
100
|
+
True
|
|
101
|
+
>>> patterns["kitchen_suites"]
|
|
102
|
+
['default', 'integration']
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
patterns: dict[str, Any] = {
|
|
106
|
+
"has_kitchen": False,
|
|
107
|
+
"has_chefspec": False,
|
|
108
|
+
"has_cookstyle": False,
|
|
109
|
+
"has_foodcritic": False,
|
|
110
|
+
"kitchen_suites": [],
|
|
111
|
+
"kitchen_platforms": [],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Check for Test Kitchen
|
|
115
|
+
kitchen_yml = cookbook_dir / ".kitchen.yml"
|
|
116
|
+
if kitchen_yml.exists():
|
|
117
|
+
patterns["has_kitchen"] = True
|
|
118
|
+
try:
|
|
119
|
+
with kitchen_yml.open() as f:
|
|
120
|
+
kitchen_config = yaml.safe_load(f)
|
|
121
|
+
if kitchen_config:
|
|
122
|
+
# Extract suites
|
|
123
|
+
suites = kitchen_config.get("suites", [])
|
|
124
|
+
if suites:
|
|
125
|
+
patterns["kitchen_suites"] = [
|
|
126
|
+
s.get("name", "default") for s in suites
|
|
127
|
+
]
|
|
128
|
+
# Extract platforms
|
|
129
|
+
platforms = kitchen_config.get("platforms", [])
|
|
130
|
+
if platforms:
|
|
131
|
+
patterns["kitchen_platforms"] = [
|
|
132
|
+
p.get("name", "unknown") for p in platforms
|
|
133
|
+
]
|
|
134
|
+
except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
|
|
135
|
+
# Gracefully handle malformed .kitchen.yml - continue with empty config
|
|
136
|
+
# Catches: YAML syntax errors, file I/O errors, missing config keys,
|
|
137
|
+
# type mismatches in config structure, and missing dict attributes
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Check for ChefSpec
|
|
141
|
+
spec_dir = cookbook_dir / "spec"
|
|
142
|
+
if spec_dir.exists() and any(spec_dir.glob("**/*_spec.rb")):
|
|
143
|
+
patterns["has_chefspec"] = True
|
|
144
|
+
|
|
145
|
+
# Check for Cookstyle
|
|
146
|
+
cookstyle_yml = cookbook_dir / ".cookstyle.yml"
|
|
147
|
+
if cookstyle_yml.exists():
|
|
148
|
+
patterns["has_cookstyle"] = True
|
|
149
|
+
|
|
150
|
+
# Check for Foodcritic (legacy)
|
|
151
|
+
if (cookbook_dir / ".foodcritic").exists():
|
|
152
|
+
patterns["has_foodcritic"] = True
|
|
153
|
+
|
|
154
|
+
return patterns
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_workflow_structure(
|
|
158
|
+
workflow_name: str,
|
|
159
|
+
patterns: dict[str, Any],
|
|
160
|
+
enable_cache: bool,
|
|
161
|
+
enable_artifacts: bool,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Build GitHub Actions workflow structure.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
workflow_name: Workflow name.
|
|
168
|
+
patterns: Detected Chef CI patterns.
|
|
169
|
+
enable_cache: Enable caching.
|
|
170
|
+
enable_artifacts: Enable artifacts.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Workflow dictionary structure.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
workflow: dict[str, Any] = {
|
|
177
|
+
"name": workflow_name,
|
|
178
|
+
"on": {
|
|
179
|
+
"push": {"branches": ["main", "develop"]},
|
|
180
|
+
"pull_request": {"branches": ["main", "develop"]},
|
|
181
|
+
},
|
|
182
|
+
"jobs": {},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Add lint job
|
|
186
|
+
if patterns["has_cookstyle"] or patterns["has_foodcritic"]:
|
|
187
|
+
workflow["jobs"]["lint"] = _build_lint_job(patterns, enable_cache)
|
|
188
|
+
|
|
189
|
+
# Add unit test job
|
|
190
|
+
if patterns["has_chefspec"]:
|
|
191
|
+
workflow["jobs"]["unit-test"] = _build_unit_test_job(enable_cache)
|
|
192
|
+
|
|
193
|
+
# Add integration test jobs
|
|
194
|
+
if patterns["has_kitchen"]:
|
|
195
|
+
workflow["jobs"]["integration-test"] = _build_integration_test_job(
|
|
196
|
+
patterns, enable_cache, enable_artifacts
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return workflow
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _build_lint_job(patterns: dict[str, Any], enable_cache: bool) -> dict[str, Any]:
|
|
203
|
+
"""
|
|
204
|
+
Build lint job configuration.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
patterns: Detected CI patterns.
|
|
208
|
+
enable_cache: Enable caching.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Lint job configuration.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
job: dict[str, Any] = {
|
|
215
|
+
"name": "Lint Cookbook",
|
|
216
|
+
"runs-on": "ubuntu-latest",
|
|
217
|
+
"steps": [
|
|
218
|
+
{"name": STEP_NAME_CHECKOUT, "uses": ACTION_CHECKOUT},
|
|
219
|
+
{
|
|
220
|
+
"name": STEP_NAME_SETUP_RUBY,
|
|
221
|
+
"uses": ACTION_SETUP_RUBY,
|
|
222
|
+
"with": {"ruby-version": "3.2"},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if enable_cache:
|
|
228
|
+
job["steps"].append(
|
|
229
|
+
{
|
|
230
|
+
"name": STEP_NAME_CACHE_GEMS,
|
|
231
|
+
"uses": ACTION_CACHE,
|
|
232
|
+
"with": {
|
|
233
|
+
"path": GEM_BUNDLE_PATH,
|
|
234
|
+
"key": GEM_CACHE_KEY,
|
|
235
|
+
"restore-keys": GEM_CACHE_RESTORE_KEY,
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
job["steps"].extend(
|
|
241
|
+
[
|
|
242
|
+
{
|
|
243
|
+
"name": STEP_NAME_INSTALL_DEPS,
|
|
244
|
+
"run": BUNDLE_INSTALL_CMD,
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Add appropriate lint commands
|
|
250
|
+
if patterns["has_cookstyle"]:
|
|
251
|
+
job["steps"].append({"name": "Run Cookstyle", "run": "bundle exec cookstyle"})
|
|
252
|
+
|
|
253
|
+
if patterns["has_foodcritic"]:
|
|
254
|
+
job["steps"].append(
|
|
255
|
+
{"name": "Run Foodcritic", "run": "bundle exec foodcritic ."}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return job
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _build_unit_test_job(enable_cache: bool) -> dict[str, Any]:
|
|
262
|
+
"""
|
|
263
|
+
Build unit test job configuration.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
enable_cache: Enable caching.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Unit test job configuration.
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
job: dict[str, Any] = {
|
|
273
|
+
"name": "Unit Tests (ChefSpec)",
|
|
274
|
+
"runs-on": "ubuntu-latest",
|
|
275
|
+
"steps": [
|
|
276
|
+
{"name": STEP_NAME_CHECKOUT, "uses": ACTION_CHECKOUT},
|
|
277
|
+
{
|
|
278
|
+
"name": STEP_NAME_SETUP_RUBY,
|
|
279
|
+
"uses": ACTION_SETUP_RUBY,
|
|
280
|
+
"with": {"ruby-version": "3.2"},
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if enable_cache:
|
|
286
|
+
job["steps"].append(
|
|
287
|
+
{
|
|
288
|
+
"name": STEP_NAME_CACHE_GEMS,
|
|
289
|
+
"uses": ACTION_CACHE,
|
|
290
|
+
"with": {
|
|
291
|
+
"path": GEM_BUNDLE_PATH,
|
|
292
|
+
"key": GEM_CACHE_KEY,
|
|
293
|
+
"restore-keys": GEM_CACHE_RESTORE_KEY,
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
job["steps"].extend(
|
|
299
|
+
[
|
|
300
|
+
{
|
|
301
|
+
"name": STEP_NAME_INSTALL_DEPS,
|
|
302
|
+
"run": BUNDLE_INSTALL_CMD,
|
|
303
|
+
},
|
|
304
|
+
{"name": "Run ChefSpec tests", "run": "bundle exec rspec"},
|
|
305
|
+
]
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return job
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _build_integration_test_job(
|
|
312
|
+
patterns: dict[str, Any], enable_cache: bool, enable_artifacts: bool
|
|
313
|
+
) -> dict[str, Any]:
|
|
314
|
+
"""
|
|
315
|
+
Build integration test job configuration.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
patterns: Detected CI patterns.
|
|
319
|
+
enable_cache: Enable caching.
|
|
320
|
+
enable_artifacts: Enable artifacts.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Integration test job configuration.
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
job: dict[str, Any] = {
|
|
327
|
+
"name": "Integration Tests (Test Kitchen)",
|
|
328
|
+
"runs-on": "ubuntu-latest",
|
|
329
|
+
"strategy": {"matrix": {"suite": patterns["kitchen_suites"] or ["default"]}},
|
|
330
|
+
"steps": [
|
|
331
|
+
{"name": STEP_NAME_CHECKOUT, "uses": ACTION_CHECKOUT},
|
|
332
|
+
{
|
|
333
|
+
"name": STEP_NAME_SETUP_RUBY,
|
|
334
|
+
"uses": ACTION_SETUP_RUBY,
|
|
335
|
+
"with": {"ruby-version": "3.2"},
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if enable_cache:
|
|
341
|
+
job["steps"].append(
|
|
342
|
+
{
|
|
343
|
+
"name": STEP_NAME_CACHE_GEMS,
|
|
344
|
+
"uses": ACTION_CACHE,
|
|
345
|
+
"with": {
|
|
346
|
+
"path": GEM_BUNDLE_PATH,
|
|
347
|
+
"key": GEM_CACHE_KEY,
|
|
348
|
+
"restore-keys": GEM_CACHE_RESTORE_KEY,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
job["steps"].extend(
|
|
354
|
+
[
|
|
355
|
+
{
|
|
356
|
+
"name": STEP_NAME_INSTALL_DEPS,
|
|
357
|
+
"run": BUNDLE_INSTALL_CMD,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"name": "Run Test Kitchen",
|
|
361
|
+
"run": "bundle exec kitchen test ${{ matrix.suite }}",
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if enable_artifacts:
|
|
367
|
+
job["steps"].append(
|
|
368
|
+
{
|
|
369
|
+
"name": "Upload test results",
|
|
370
|
+
"uses": ACTION_UPLOAD_ARTIFACT,
|
|
371
|
+
"if": "always()",
|
|
372
|
+
"with": {
|
|
373
|
+
"name": "kitchen-logs-${{ matrix.suite }}",
|
|
374
|
+
"path": ".kitchen/logs/",
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return job
|
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
|
+
# Analyze Chef CI patterns
|
|
31
|
+
ci_patterns = _analyze_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 _analyze_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Analyze 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)
|