@intentsolutionsio/devops-automation-pack 1.0.0

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.
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Suggest the appropriate commit type based on code changes.
4
+
5
+ This script analyzes code changes and suggests the most appropriate
6
+ commit type from the conventional commits specification (feat, fix,
7
+ docs, style, refactor, perf, test, chore, ci, build, revert).
8
+ """
9
+
10
+ import argparse
11
+ import sys
12
+ import subprocess
13
+ import json
14
+ from typing import Tuple, Dict, List, Optional
15
+
16
+
17
+ def get_git_diff(staged_only: bool = False, ref: Optional[str] = None) -> str:
18
+ """
19
+ Get git diff content.
20
+
21
+ Args:
22
+ staged_only: If True, get staged changes only
23
+ ref: Optional reference to compare against (e.g., 'main', 'HEAD~1')
24
+
25
+ Returns:
26
+ The git diff output
27
+ """
28
+ try:
29
+ cmd = ['git', 'diff']
30
+ if staged_only:
31
+ cmd.append('--cached')
32
+ if ref:
33
+ cmd.append(ref)
34
+
35
+ result = subprocess.run(
36
+ cmd,
37
+ capture_output=True,
38
+ text=True,
39
+ check=False
40
+ )
41
+
42
+ return result.stdout if result.returncode == 0 else ""
43
+ except FileNotFoundError:
44
+ return ""
45
+
46
+
47
+ def get_diff_from_file(filepath: str) -> str:
48
+ """Read diff from a file."""
49
+ try:
50
+ with open(filepath, 'r', encoding='utf-8') as f:
51
+ return f.read()
52
+ except FileNotFoundError:
53
+ print(f"Error: File not found: {filepath}", file=sys.stderr)
54
+ sys.exit(1)
55
+ except IOError as e:
56
+ print(f"Error reading file: {e}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+
60
+ def analyze_changes(diff_content: str) -> Dict[str, int]:
61
+ """
62
+ Count different types of changes in the diff.
63
+
64
+ Args:
65
+ diff_content: The git diff content
66
+
67
+ Returns:
68
+ Dictionary of change type counts
69
+ """
70
+ counts = {
71
+ 'test_additions': 0,
72
+ 'test_removals': 0,
73
+ 'doc_changes': 0,
74
+ 'style_changes': 0,
75
+ 'feature_additions': 0,
76
+ 'feature_removals': 0,
77
+ 'performance_changes': 0,
78
+ 'bug_references': 0,
79
+ 'refactor_changes': 0,
80
+ 'ci_changes': 0,
81
+ 'dependency_changes': 0,
82
+ 'total_additions': 0,
83
+ 'total_removals': 0,
84
+ }
85
+
86
+ lines = diff_content.split('\n')
87
+ current_file = ''
88
+
89
+ for i, line in enumerate(lines):
90
+ # Track current file
91
+ if line.startswith('diff --git'):
92
+ parts = line.split(' ')
93
+ if len(parts) >= 4:
94
+ current_file = parts[3]
95
+
96
+ # Count additions and removals
97
+ if line.startswith('+') and not line.startswith('+++'):
98
+ counts['total_additions'] += 1
99
+
100
+ # Analyze added lines
101
+ content = line[1:]
102
+ if 'test' in current_file.lower() or 'spec' in current_file.lower():
103
+ counts['test_additions'] += 1
104
+ elif 'package.json' in current_file or 'requirements.txt' in current_file:
105
+ counts['dependency_changes'] += 1
106
+ elif any(x in content.lower() for x in ['def ', 'class ', 'function ', 'const ', 'async ']):
107
+ counts['feature_additions'] += 1
108
+ elif any(x in content.lower() for x in ['optimize', 'performance', 'cache', 'lazy']):
109
+ counts['performance_changes'] += 1
110
+
111
+ elif line.startswith('-') and not line.startswith('---'):
112
+ counts['total_removals'] += 1
113
+
114
+ content = line[1:]
115
+ if 'test' in current_file.lower() or 'spec' in current_file.lower():
116
+ counts['test_removals'] += 1
117
+ elif any(x in content.lower() for x in ['def ', 'class ', 'function ', 'const ']):
118
+ counts['feature_removals'] += 1
119
+
120
+ # Detect specific change patterns
121
+ if any(x in line.lower() for x in ['.md', 'readme', 'docs/', 'documentation']):
122
+ counts['doc_changes'] += 1
123
+
124
+ # Style changes (mostly whitespace/formatting)
125
+ if line.startswith('+') or line.startswith('-'):
126
+ if len(line.lstrip('+-').strip()) < 10: # Short lines
127
+ counts['style_changes'] += 1
128
+
129
+ # Bug/fix references
130
+ if any(x in line.lower() for x in ['fix', 'bug', 'issue', 'closes #', 'fixes #']):
131
+ counts['bug_references'] += 1
132
+
133
+ # CI/CD changes
134
+ if '.github/workflows' in current_file or '.gitlab-ci' in current_file or 'Jenkinsfile' in current_file:
135
+ counts['ci_changes'] += 1
136
+ if 'pipeline' in line.lower() or 'workflow' in line.lower():
137
+ counts['ci_changes'] += 1
138
+
139
+ # Refactoring (renaming, restructuring without functional change)
140
+ if any(x in line.lower() for x in ['rename', 'reorganize', 'refactor', 'restructure']):
141
+ counts['refactor_changes'] += 1
142
+
143
+ return counts
144
+
145
+
146
+ def suggest_commit_type(diff_content: str) -> Tuple[str, float, str]:
147
+ """
148
+ Suggest the most appropriate commit type.
149
+
150
+ Args:
151
+ diff_content: The git diff content
152
+
153
+ Returns:
154
+ Tuple of (suggested_type, confidence, reasoning)
155
+ """
156
+ changes = analyze_changes(diff_content)
157
+
158
+ # Scoring system: higher score = more confident
159
+ scores = {
160
+ 'feat': 0.0,
161
+ 'fix': 0.0,
162
+ 'docs': 0.0,
163
+ 'style': 0.0,
164
+ 'refactor': 0.0,
165
+ 'perf': 0.0,
166
+ 'test': 0.0,
167
+ 'chore': 0.0,
168
+ 'ci': 0.0,
169
+ 'build': 0.0,
170
+ }
171
+
172
+ reasoning = []
173
+
174
+ # Test changes
175
+ if changes['test_additions'] > 0:
176
+ scores['test'] += changes['test_additions'] * 2
177
+ reasoning.append(f"Added {changes['test_additions']} test lines")
178
+
179
+ # Documentation changes
180
+ if changes['doc_changes'] > 0:
181
+ scores['docs'] += changes['doc_changes'] * 1.5
182
+ reasoning.append(f"Modified {changes['doc_changes']} documentation lines")
183
+
184
+ # Bug fixes
185
+ if changes['bug_references'] > 0:
186
+ scores['fix'] += changes['bug_references'] * 3
187
+ reasoning.append(f"Found {changes['bug_references']} bug/fix references")
188
+
189
+ # Performance improvements
190
+ if changes['performance_changes'] > 0:
191
+ scores['perf'] += changes['performance_changes'] * 2
192
+ reasoning.append(f"Found {changes['performance_changes']} performance improvements")
193
+
194
+ # CI/CD changes
195
+ if changes['ci_changes'] > 0:
196
+ scores['ci'] += changes['ci_changes'] * 2
197
+ reasoning.append(f"Modified CI/CD files ({changes['ci_changes']} lines)")
198
+
199
+ # Dependency changes
200
+ if changes['dependency_changes'] > 0:
201
+ scores['build'] += changes['dependency_changes'] * 1.5
202
+ reasoning.append(f"Modified dependencies ({changes['dependency_changes']} changes)")
203
+
204
+ # Feature additions
205
+ if changes['feature_additions'] > changes['feature_removals']:
206
+ feature_ratio = changes['feature_additions'] / max(1, changes['feature_removals'])
207
+ scores['feat'] += feature_ratio * 2
208
+ reasoning.append(f"Added {changes['feature_additions']} feature lines")
209
+
210
+ # Refactoring
211
+ if changes['refactor_changes'] > 0:
212
+ scores['refactor'] += changes['refactor_changes'] * 1.5
213
+ reasoning.append(f"Found {changes['refactor_changes']} refactoring indicators")
214
+
215
+ # Style changes
216
+ if changes['style_changes'] > changes['total_additions'] * 0.3:
217
+ scores['style'] += changes['style_changes']
218
+ reasoning.append(f"Mostly style changes ({changes['style_changes']} lines)")
219
+
220
+ # Fallback: determine based on change volume
221
+ total_changes = changes['total_additions'] + changes['total_removals']
222
+ if total_changes == 0:
223
+ return 'chore', 0.5, "No meaningful changes detected"
224
+
225
+ # If nothing scored highly, use refactor as catch-all
226
+ if max(scores.values()) == 0:
227
+ scores['refactor'] = 1.0
228
+ reasoning.append("General code changes without specific category")
229
+
230
+ # Find the highest scoring type
231
+ suggested = max(scores, key=scores.get)
232
+ confidence = scores[suggested] / max(1, total_changes)
233
+ confidence = min(1.0, confidence) # Cap at 1.0
234
+
235
+ reasoning_str = " | ".join(reasoning) if reasoning else "General code changes"
236
+
237
+ return suggested, confidence, reasoning_str
238
+
239
+
240
+ def format_output(
241
+ suggested_type: str,
242
+ confidence: float,
243
+ reasoning: str,
244
+ format_type: str = 'text'
245
+ ) -> str:
246
+ """Format the output based on requested format."""
247
+ if format_type == 'json':
248
+ return json.dumps({
249
+ 'type': suggested_type,
250
+ 'confidence': round(confidence, 2),
251
+ 'confidence_percent': f"{confidence * 100:.1f}%",
252
+ 'reasoning': reasoning
253
+ }, indent=2)
254
+ else:
255
+ confidence_percent = f"{confidence * 100:.1f}%"
256
+ return (
257
+ f"Suggested commit type: {suggested_type}\n"
258
+ f"Confidence: {confidence_percent}\n"
259
+ f"Reasoning: {reasoning}"
260
+ )
261
+
262
+
263
+ def main():
264
+ """Main entry point."""
265
+ parser = argparse.ArgumentParser(
266
+ description="Suggest the appropriate commit type based on code changes",
267
+ formatter_class=argparse.RawDescriptionHelpFormatter,
268
+ epilog="""
269
+ Examples:
270
+ # Suggest type from staged changes
271
+ %(prog)s --staged
272
+
273
+ # Suggest type from unstaged changes
274
+ %(prog)s --unstaged
275
+
276
+ # Suggest type from a diff file
277
+ %(prog)s --file changes.diff
278
+
279
+ # Compare against a specific branch
280
+ %(prog)s --ref main
281
+
282
+ # Output as JSON
283
+ %(prog)s --staged --format json
284
+
285
+ # Verbose output with detailed analysis
286
+ %(prog)s --staged --verbose
287
+ """
288
+ )
289
+
290
+ input_group = parser.add_mutually_exclusive_group()
291
+ input_group.add_argument(
292
+ '--staged',
293
+ action='store_true',
294
+ help='Analyze staged changes (default)'
295
+ )
296
+ input_group.add_argument(
297
+ '--unstaged',
298
+ action='store_true',
299
+ help='Analyze unstaged changes'
300
+ )
301
+ input_group.add_argument(
302
+ '--file',
303
+ type=str,
304
+ help='Path to diff file to analyze'
305
+ )
306
+
307
+ parser.add_argument(
308
+ '--ref',
309
+ type=str,
310
+ help='Git reference to compare against (e.g., main, HEAD~1)'
311
+ )
312
+ parser.add_argument(
313
+ '--format',
314
+ choices=['text', 'json'],
315
+ default='text',
316
+ help='Output format (default: text)'
317
+ )
318
+ parser.add_argument(
319
+ '-v', '--verbose',
320
+ action='store_true',
321
+ help='Enable verbose output with detailed analysis'
322
+ )
323
+
324
+ args = parser.parse_args()
325
+
326
+ # Get diff content
327
+ if args.file:
328
+ diff_content = get_diff_from_file(args.file)
329
+ elif args.unstaged:
330
+ diff_content = get_git_diff(staged_only=False, ref=args.ref)
331
+ else: # Default to staged
332
+ diff_content = get_git_diff(staged_only=True, ref=args.ref)
333
+
334
+ if not diff_content:
335
+ print("Error: No changes to analyze", file=sys.stderr)
336
+ return 1
337
+
338
+ # Suggest commit type
339
+ suggested_type, confidence, reasoning = suggest_commit_type(diff_content)
340
+
341
+ # Output result
342
+ output = format_output(suggested_type, confidence, reasoning, args.format)
343
+ print(output)
344
+
345
+ return 0
346
+
347
+
348
+ if __name__ == '__main__':
349
+ sys.exit(main())
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate commit messages against the Conventional Commits specification.
4
+
5
+ This script validates that commit messages follow the conventional commits
6
+ standard, which includes type(scope): subject format with proper structure.
7
+
8
+ The conventional commits specification defines:
9
+ - type: feat, fix, docs, style, refactor, perf, test, chore, ci, etc.
10
+ - scope: optional area of the codebase
11
+ - subject: brief description of the change
12
+ """
13
+
14
+ import argparse
15
+ import sys
16
+ import re
17
+ from typing import Tuple
18
+
19
+
20
+ def validate_conventional_commit(message: str) -> Tuple[bool, str]:
21
+ """
22
+ Validate a commit message against conventional commits specification.
23
+
24
+ Args:
25
+ message: The commit message to validate
26
+
27
+ Returns:
28
+ Tuple of (is_valid, error_message)
29
+ - is_valid: True if message is valid, False otherwise
30
+ - error_message: Description of validation error (empty if valid)
31
+ """
32
+ if not message or not message.strip():
33
+ return False, "Commit message cannot be empty"
34
+
35
+ message = message.strip()
36
+ lines = message.split('\n')
37
+ subject = lines[0]
38
+
39
+ # Validate subject line format
40
+ # Pattern: type(scope)?: subject or type: subject
41
+ # Where type is required, scope is optional, and subject is required
42
+ pattern = r'^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .{1,}$'
43
+
44
+ if not re.match(pattern, subject):
45
+ return False, (
46
+ f"Invalid commit message format: '{subject}'\n"
47
+ "Expected format: type(scope): subject\n"
48
+ "Valid types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert\n"
49
+ "Example: feat(auth): add login functionality"
50
+ )
51
+
52
+ # Validate subject line length (recommended max 50 chars)
53
+ if len(subject) > 72:
54
+ return False, (
55
+ f"Subject line too long ({len(subject)} chars). "
56
+ "Recommended maximum is 50 characters, hard limit is 72"
57
+ )
58
+
59
+ # Validate subject doesn't end with period
60
+ if subject.endswith('.'):
61
+ return False, "Subject line should not end with a period"
62
+
63
+ # If there are multiple lines, validate blank line after subject
64
+ if len(lines) > 1:
65
+ if lines[1].strip() != '':
66
+ return False, "Expected blank line between subject and body"
67
+
68
+ return True, ""
69
+
70
+
71
+ def main():
72
+ """Main entry point for the validation script."""
73
+ parser = argparse.ArgumentParser(
74
+ description="Validate commit messages against the Conventional Commits specification",
75
+ formatter_class=argparse.RawDescriptionHelpFormatter,
76
+ epilog="""
77
+ Examples:
78
+ # Validate a commit message from a file
79
+ %(prog)s --file commit-msg.txt
80
+
81
+ # Validate a commit message from command line
82
+ %(prog)s --message "feat(api): add new endpoint"
83
+
84
+ # Validate with verbose output
85
+ %(prog)s --message "fix: resolve bug" --verbose
86
+ """
87
+ )
88
+
89
+ # Create mutually exclusive group for input source
90
+ input_group = parser.add_mutually_exclusive_group(required=True)
91
+ input_group.add_argument(
92
+ '-m', '--message',
93
+ type=str,
94
+ help='Commit message to validate'
95
+ )
96
+ input_group.add_argument(
97
+ '-f', '--file',
98
+ type=str,
99
+ help='Path to file containing commit message'
100
+ )
101
+
102
+ parser.add_argument(
103
+ '-v', '--verbose',
104
+ action='store_true',
105
+ help='Enable verbose output'
106
+ )
107
+
108
+ args = parser.parse_args()
109
+
110
+ # Get the commit message
111
+ if args.file:
112
+ try:
113
+ with open(args.file, 'r', encoding='utf-8') as f:
114
+ message = f.read()
115
+ except FileNotFoundError:
116
+ print(f"Error: File not found: {args.file}", file=sys.stderr)
117
+ return 1
118
+ except IOError as e:
119
+ print(f"Error reading file: {e}", file=sys.stderr)
120
+ return 1
121
+ else:
122
+ message = args.message
123
+
124
+ # Validate the message
125
+ is_valid, error_msg = validate_conventional_commit(message)
126
+
127
+ if is_valid:
128
+ if args.verbose:
129
+ print("✓ Commit message is valid")
130
+ return 0
131
+ else:
132
+ print(f"✗ Validation failed:", file=sys.stderr)
133
+ print(error_msg, file=sys.stderr)
134
+ return 1
135
+
136
+
137
+ if __name__ == '__main__':
138
+ sys.exit(main())