docopslab-dev 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +904 -0
- data/assets/config-packs/actionlint/base.yml +13 -0
- data/assets/config-packs/actionlint/project.yml +13 -0
- data/assets/config-packs/htmlproofer/base.yml +27 -0
- data/assets/config-packs/htmlproofer/project.yml +25 -0
- data/assets/config-packs/rubocop/base.yml +130 -0
- data/assets/config-packs/rubocop/project.yml +8 -0
- data/assets/config-packs/shellcheck/base.shellcheckrc +14 -0
- data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +11 -0
- data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +7 -0
- data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +8 -0
- data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +8 -0
- data/assets/config-packs/vale/asciidoc/ProperDLs.yml +7 -0
- data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +8 -0
- data/assets/config-packs/vale/authoring/ButParagraph.yml +8 -0
- data/assets/config-packs/vale/authoring/ExNotEg.yml +8 -0
- data/assets/config-packs/vale/authoring/LiteralTerms.yml +20 -0
- data/assets/config-packs/vale/authoring/Spelling.yml +679 -0
- data/assets/config-packs/vale/base.ini +38 -0
- data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +56 -0
- data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +121 -0
- data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +53 -0
- data/assets/config-packs/vale/project.ini +5 -0
- data/assets/hooks/pre-commit +63 -0
- data/assets/hooks/pre-push +72 -0
- data/assets/scripts/adoc_section_ids.rb +50 -0
- data/assets/scripts/build-common.sh +193 -0
- data/assets/scripts/build-docker.sh +64 -0
- data/assets/scripts/build.sh +56 -0
- data/assets/scripts/parse_jekyll_asciidoc_logs.rb +467 -0
- data/assets/templates/Gemfile +7 -0
- data/assets/templates/Rakefile +3 -0
- data/assets/templates/gitignore +69 -0
- data/assets/templates/jekyll-asciidoc-fix.prompt.yml +17 -0
- data/assets/templates/spellcheck.prompt.yml +16 -0
- data/docopslab-dev.gemspec +56 -0
- data/docs/agent/AGENTS.md +229 -0
- data/docs/agent/index.md +80 -0
- data/docs/agent/missions/conduct-release.md +224 -0
- data/docs/agent/missions/setup-new-project.md +250 -0
- data/docs/agent/roles/devops-release-engineer.md +152 -0
- data/docs/agent/roles/docops-engineer.md +193 -0
- data/docs/agent/roles/planner-architect.md +74 -0
- data/docs/agent/roles/product-engineer.md +153 -0
- data/docs/agent/roles/product-manager.md +130 -0
- data/docs/agent/roles/project-manager.md +139 -0
- data/docs/agent/roles/qa-testing-engineer.md +115 -0
- data/docs/agent/roles/tech-docs-manager.md +143 -0
- data/docs/agent/roles/tech-writer.md +163 -0
- data/docs/agent/skills/asciidoc.md +609 -0
- data/docs/agent/skills/code-commenting.md +347 -0
- data/docs/agent/skills/fix-broken-links.md +309 -0
- data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +23 -0
- data/docs/agent/skills/fix-spelling-issues.md +13 -0
- data/docs/agent/skills/git.md +170 -0
- data/docs/agent/skills/github-issues.md +135 -0
- data/docs/agent/skills/product-release-rollback-and-patching.md +71 -0
- data/docs/agent/skills/rake-cli-dev.md +57 -0
- data/docs/agent/skills/readme-driven-dev.md +13 -0
- data/docs/agent/skills/release-history.md +29 -0
- data/docs/agent/skills/ruby.md +192 -0
- data/docs/agent/skills/schemagraphy-sgyml.md +18 -0
- data/docs/agent/skills/tests-running.md +25 -0
- data/docs/agent/skills/tests-writing.md +45 -0
- data/docs/agent/skills/write-the-docs.md +54 -0
- data/docs/agent/topics/common-project-paths.md +117 -0
- data/docs/agent/topics/dev-tooling-usage.md +202 -0
- data/docs/agent/topics/devops-ci-cd.md +55 -0
- data/docs/agent/topics/product-docs-deployment.md +25 -0
- data/lib/docopslab/dev/auto_fix_asciidoc.rb +46 -0
- data/lib/docopslab/dev/checkers.rb +108 -0
- data/lib/docopslab/dev/config_manager.rb +241 -0
- data/lib/docopslab/dev/file_utils.rb +140 -0
- data/lib/docopslab/dev/git_hooks.rb +140 -0
- data/lib/docopslab/dev/help.rb +121 -0
- data/lib/docopslab/dev/initializer.rb +95 -0
- data/lib/docopslab/dev/linters.rb +451 -0
- data/lib/docopslab/dev/log_parser.rb +31 -0
- data/lib/docopslab/dev/paths.rb +46 -0
- data/lib/docopslab/dev/script_manager.rb +136 -0
- data/lib/docopslab/dev/spell_check.rb +194 -0
- data/lib/docopslab/dev/sync_ops.rb +468 -0
- data/lib/docopslab/dev/tasks.rb +440 -0
- data/lib/docopslab/dev/tool_execution.rb +68 -0
- data/lib/docopslab/dev/version.rb +8 -0
- data/lib/docopslab/dev.rb +392 -0
- data/specs/data/default-manifest.yml +64 -0
- data/specs/data/manifest-schema.yaml +63 -0
- data/specs/data/tasks-def.yml +321 -0
- data/specs/data/tools.yml +60 -0
- metadata +362 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocOpsLab
|
|
4
|
+
module Dev
|
|
5
|
+
module ConfigManager
|
|
6
|
+
class << self
|
|
7
|
+
def generate_vale_config _context, style_override: nil
|
|
8
|
+
base_config = File.join(CONFIG_VENDOR_DIR, 'vale.ini')
|
|
9
|
+
project_config = '.config/vale.local.ini'
|
|
10
|
+
generated_config = CONFIG_PATHS[:vale]
|
|
11
|
+
|
|
12
|
+
return false unless File.exist?(base_config)
|
|
13
|
+
|
|
14
|
+
merged_content = if File.exist?(project_config)
|
|
15
|
+
merge_ini_configs(base_config, project_config)
|
|
16
|
+
else
|
|
17
|
+
File.read(base_config)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Apply runtime style override if specified
|
|
21
|
+
merged_content = apply_vale_style_override(merged_content, style_override) if style_override
|
|
22
|
+
|
|
23
|
+
# Write generated config
|
|
24
|
+
if !File.exist?(generated_config) || File.read(generated_config) != merged_content
|
|
25
|
+
File.write(generated_config, merged_content)
|
|
26
|
+
override_msg = style_override ? " (#{style_override} styles)" : ''
|
|
27
|
+
puts " 📝 Generated #{generated_config} from base#{if File.exist?(project_config)
|
|
28
|
+
' + local'
|
|
29
|
+
end}#{override_msg}"
|
|
30
|
+
true
|
|
31
|
+
else
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generate_htmlproofer_config _context
|
|
37
|
+
base_config = File.join(CONFIG_VENDOR_DIR, 'htmlproofer.yml')
|
|
38
|
+
project_config = '.config/htmlproofer.local.yml'
|
|
39
|
+
generated_config = CONFIG_PATHS[:htmlproofer]
|
|
40
|
+
|
|
41
|
+
return false unless File.exist?(base_config)
|
|
42
|
+
|
|
43
|
+
merged_content = if File.exist?(project_config)
|
|
44
|
+
merge_yaml_configs(base_config, project_config)
|
|
45
|
+
else
|
|
46
|
+
File.read(base_config)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if !File.exist?(generated_config) || File.read(generated_config) != merged_content
|
|
50
|
+
File.write(generated_config, merged_content)
|
|
51
|
+
puts " 📝 Generated #{generated_config} from base#{' + local' if File.exist?(project_config)}"
|
|
52
|
+
true
|
|
53
|
+
else
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load_htmlproofer_config config_path=nil, policy: 'merge'
|
|
59
|
+
config_paths = if config_path && File.exist?(config_path)
|
|
60
|
+
[config_path]
|
|
61
|
+
else
|
|
62
|
+
[CONFIG_PATHS[:htmlproofer]]
|
|
63
|
+
end
|
|
64
|
+
config_paths << File.join(CONFIG_VENDOR_DIR, 'htmlproofer.yml') unless policy == 'replace'
|
|
65
|
+
config_path = config_paths.find { |path| File.exist?(path) }
|
|
66
|
+
|
|
67
|
+
return unless config_path
|
|
68
|
+
|
|
69
|
+
puts "📋 Using HTMLProofer config: #{config_path}"
|
|
70
|
+
begin
|
|
71
|
+
config = YAML.load_file(config_path)
|
|
72
|
+
# Convert string patterns to regex for ignore_urls and ignore_files
|
|
73
|
+
process_htmlproofer_patterns(config)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
puts "⚠️ Failed to load #{config_path}: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def process_htmlproofer_patterns config
|
|
80
|
+
# Convert string patterns to regex for ignore_urls
|
|
81
|
+
if config['ignore_urls'].is_a?(Array)
|
|
82
|
+
config['ignore_urls'] = config['ignore_urls'].map do |pattern|
|
|
83
|
+
if pattern.is_a?(String) && pattern.start_with?('/') && pattern.end_with?('/')
|
|
84
|
+
Regexp.new(pattern[1..-2])
|
|
85
|
+
else
|
|
86
|
+
pattern
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Convert string patterns to regex for ignore_files
|
|
92
|
+
if config['ignore_files'].is_a?(Array)
|
|
93
|
+
config['ignore_files'] = config['ignore_files'].map do |pattern|
|
|
94
|
+
pattern.is_a?(String) ? Regexp.new(pattern) : pattern
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Convert string keys to symbols for HTMLProofer
|
|
99
|
+
config.transform_keys(&:to_sym)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def merge_yaml_configs base_path, local_path
|
|
103
|
+
# Implement RuboCop-style inheritance for YAML files
|
|
104
|
+
require 'yaml'
|
|
105
|
+
|
|
106
|
+
base_config = YAML.load_file(base_path) || {}
|
|
107
|
+
project_config = YAML.load_file(local_path) || {}
|
|
108
|
+
|
|
109
|
+
# Merge with RuboCop semantics
|
|
110
|
+
merged_config = deep_merge_configs(base_config, project_config)
|
|
111
|
+
|
|
112
|
+
# Convert back to YAML
|
|
113
|
+
YAML.dump(merged_config)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def deep_merge_configs base, local
|
|
117
|
+
return local if base.nil?
|
|
118
|
+
return base if local.nil?
|
|
119
|
+
|
|
120
|
+
case local
|
|
121
|
+
when Hash
|
|
122
|
+
result = base.is_a?(Hash) ? base.dup : {}
|
|
123
|
+
local.each do |key, value|
|
|
124
|
+
if value.nil? # YAML null (~) cancels the setting
|
|
125
|
+
result.delete(key)
|
|
126
|
+
else
|
|
127
|
+
result[key] = deep_merge_configs(result[key], value)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
result
|
|
131
|
+
else
|
|
132
|
+
# Non-hash values: local completely overrides base (including arrays)
|
|
133
|
+
local
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def generate_simple_ini config
|
|
138
|
+
lines = []
|
|
139
|
+
|
|
140
|
+
# Global section first
|
|
141
|
+
if config['global'] && !config['global'].empty?
|
|
142
|
+
config['global'].each do |key, value|
|
|
143
|
+
lines << "#{key} = #{value}"
|
|
144
|
+
end
|
|
145
|
+
lines << ''
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Other sections
|
|
149
|
+
config.each do |section_name, section_data|
|
|
150
|
+
next if section_name == 'global'
|
|
151
|
+
next if section_data.empty?
|
|
152
|
+
|
|
153
|
+
lines << "[#{section_name}]"
|
|
154
|
+
section_data.each do |key, value|
|
|
155
|
+
lines << "#{key} = #{value}"
|
|
156
|
+
end
|
|
157
|
+
lines << ''
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
"#{lines.join("\n").strip}\n"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def get_path_config tool_slug, context
|
|
164
|
+
tool_meta = context.get_tool_metadata(tool_slug)
|
|
165
|
+
default_config = tool_meta&.dig('paths') || {}
|
|
166
|
+
|
|
167
|
+
manifest = context.load_manifest
|
|
168
|
+
project_config = manifest&.dig('tools')&.find { |t| t['tool'] == tool_slug }&.dig('paths') || {}
|
|
169
|
+
|
|
170
|
+
git_tracked_only = if project_config.key?('git_tracked_only')
|
|
171
|
+
project_config['git_tracked_only']
|
|
172
|
+
else
|
|
173
|
+
default_config.fetch('git_tracked_only', true)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Project-level 'lint'/'skip' overrides gem-level 'patterns'/'ignored_paths'
|
|
177
|
+
lint_paths = project_config['lint'] || default_config['patterns']
|
|
178
|
+
skip_paths = (project_config['skip'] || []) + (default_config['ignored_paths'] || [])
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
lint: lint_paths,
|
|
182
|
+
skip: skip_paths.uniq,
|
|
183
|
+
exts: project_config['exts'] || default_config['exts'],
|
|
184
|
+
git_tracked_only: git_tracked_only
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def merge_ini_configs base_path, local_path
|
|
189
|
+
# Simple but working INI merger; good enough for our needs
|
|
190
|
+
base_config = parse_simple_ini(File.read(base_path))
|
|
191
|
+
project_config = parse_simple_ini(File.read(local_path))
|
|
192
|
+
|
|
193
|
+
# Merge with RuboCop semantics: local overrides base, sections merge
|
|
194
|
+
merged_config = deep_merge_configs(base_config, project_config)
|
|
195
|
+
|
|
196
|
+
# Convert back to INI format
|
|
197
|
+
generate_simple_ini(merged_config)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def parse_simple_ini content
|
|
201
|
+
config = { 'global' => {} }
|
|
202
|
+
current_section = 'global'
|
|
203
|
+
|
|
204
|
+
content.lines.each do |line|
|
|
205
|
+
line = line.strip
|
|
206
|
+
next if line.empty? || line.start_with?('#')
|
|
207
|
+
|
|
208
|
+
if line =~ /^\[(.+)\]$/
|
|
209
|
+
current_section = ::Regexp.last_match(1)
|
|
210
|
+
config[current_section] = {}
|
|
211
|
+
elsif line =~ /^([^=]+)\s*=\s*(.*)$/
|
|
212
|
+
key = ::Regexp.last_match(1).strip
|
|
213
|
+
value = ::Regexp.last_match(2).strip
|
|
214
|
+
config[current_section][key] = value
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
config
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def apply_vale_style_override content, override_type
|
|
222
|
+
# Parse the INI content
|
|
223
|
+
config = parse_simple_ini(content)
|
|
224
|
+
|
|
225
|
+
# Apply the override to the [*.adoc] section
|
|
226
|
+
if config['*.adoc']
|
|
227
|
+
case override_type
|
|
228
|
+
when :text
|
|
229
|
+
config['*.adoc']['BasedOnStyles'] = 'RedHat, DocOpsLab-Authoring'
|
|
230
|
+
when :adoc
|
|
231
|
+
config['*.adoc']['BasedOnStyles'] = 'AsciiDoc, DocOpsLab-AsciiDoc'
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Convert back to INI format
|
|
236
|
+
generate_simple_ini(config)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module DocOpsLab
|
|
6
|
+
module Dev
|
|
7
|
+
module FileUtilities
|
|
8
|
+
class << self
|
|
9
|
+
def find_shell_scripts context
|
|
10
|
+
# First, try to find files using the new path configuration system
|
|
11
|
+
files = find_files_to_lint('shellcheck', context)
|
|
12
|
+
return files if files && !files.empty?
|
|
13
|
+
|
|
14
|
+
# Fallback to old method if no paths are configured for shellcheck
|
|
15
|
+
scripts = []
|
|
16
|
+
patterns = [
|
|
17
|
+
'**/*.sh',
|
|
18
|
+
'**/*.bash',
|
|
19
|
+
'**/.*rc',
|
|
20
|
+
'**/.*profile',
|
|
21
|
+
'scripts/*.sh'
|
|
22
|
+
]
|
|
23
|
+
patterns.each do |pattern|
|
|
24
|
+
Dir.glob(pattern).each do |file|
|
|
25
|
+
next unless File.file?(file)
|
|
26
|
+
next if file.include?('/.vendor/')
|
|
27
|
+
next if file.include?('/node_modules/')
|
|
28
|
+
next unless FileUtilities.git_tracked_or_staged?(file)
|
|
29
|
+
|
|
30
|
+
scripts << file if File.executable?(file) || FileUtilities.shell_shebang?(file)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
scripts.uniq.sort
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def shell_shebang? file
|
|
37
|
+
return false unless File.readable?(file)
|
|
38
|
+
|
|
39
|
+
first_line = File.open(file, 'r') do |f|
|
|
40
|
+
f.readline
|
|
41
|
+
rescue StandardError
|
|
42
|
+
''
|
|
43
|
+
end
|
|
44
|
+
first_line.start_with?('#!') &&
|
|
45
|
+
(first_line.include?('sh') || first_line.include?('bash'))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find_files_to_lint tool_slug, context
|
|
49
|
+
path_config = context.get_path_config(tool_slug)
|
|
50
|
+
lint_paths = path_config[:lint]
|
|
51
|
+
skip_paths = path_config[:skip]
|
|
52
|
+
exts = path_config[:exts]
|
|
53
|
+
git_tracked_only = path_config[:git_tracked_only]
|
|
54
|
+
|
|
55
|
+
return [] unless lint_paths
|
|
56
|
+
|
|
57
|
+
files = []
|
|
58
|
+
lint_paths.each do |path|
|
|
59
|
+
# If path is a directory, search recursively. Otherwise, it's a glob.
|
|
60
|
+
glob_pattern = File.directory?(path) ? File.join(path, '**', '*') : path
|
|
61
|
+
Dir.glob(glob_pattern).each do |file|
|
|
62
|
+
next unless File.file?(file)
|
|
63
|
+
|
|
64
|
+
# Normalize path by removing ./ prefix for consistent pattern matching
|
|
65
|
+
normalized = file.sub(%r{^\./}, '')
|
|
66
|
+
files << normalized
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
files.uniq!
|
|
71
|
+
|
|
72
|
+
# Filter by extension if exts is provided
|
|
73
|
+
if exts && !exts.empty?
|
|
74
|
+
files.select! do |file|
|
|
75
|
+
ext = File.extname(file).delete_prefix('.')
|
|
76
|
+
exts.include?(ext)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Filter out ignored paths
|
|
81
|
+
files.reject! do |file|
|
|
82
|
+
should_skip = skip_paths.any? do |ignored|
|
|
83
|
+
FileUtilities.file_matches_ignore_pattern?(file, ignored)
|
|
84
|
+
end
|
|
85
|
+
should_skip
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Filter by git tracking status
|
|
89
|
+
if git_tracked_only
|
|
90
|
+
files.select! do |file|
|
|
91
|
+
is_tracked = FileUtilities.git_tracked_or_staged?(file)
|
|
92
|
+
is_tracked
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
files.sort
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def find_asciidoc_files context
|
|
100
|
+
FileUtilities.find_files_to_lint('vale', context)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def file_matches_ignore_pattern? file, pattern
|
|
104
|
+
if pattern.include?('*') || pattern.include?('?')
|
|
105
|
+
# Handle glob patterns
|
|
106
|
+
# If pattern ends with /*, treat it as recursive (dir/**/*)
|
|
107
|
+
recursive_pattern = if pattern.end_with?('/*')
|
|
108
|
+
pattern.sub(%r{/\*$}, '/**/*')
|
|
109
|
+
else
|
|
110
|
+
pattern
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
File.fnmatch(recursive_pattern, file, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
|
|
114
|
+
# Also try exact match without modification for explicit patterns
|
|
115
|
+
File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
116
|
+
else
|
|
117
|
+
# Non-glob patterns: match directory name anywhere in path
|
|
118
|
+
File.fnmatch("**/#{pattern}/**", file, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
|
|
119
|
+
File.fnmatch("**/#{pattern}", file, File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def git_tracked_or_staged? file
|
|
124
|
+
return true unless Dir.exist?('.git')
|
|
125
|
+
|
|
126
|
+
repo_root = `git rev-parse --show-toplevel`.strip
|
|
127
|
+
rel = Pathname.new(file).expand_path.relative_path_from(Pathname.new(repo_root)).to_s
|
|
128
|
+
|
|
129
|
+
# Check if the file is tracked
|
|
130
|
+
return true if system('git', 'ls-files', '--error-unmatch', rel, out: File::NULL, err: File::NULL)
|
|
131
|
+
|
|
132
|
+
# Check if the file is staged (but not necessarily committed yet)
|
|
133
|
+
return true if system('git', 'diff', '--name-only', '--cached', '--', rel, out: File::NULL, err: File::NULL)
|
|
134
|
+
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module DocOpsLab
|
|
6
|
+
module Dev
|
|
7
|
+
module GitHooks
|
|
8
|
+
class << self
|
|
9
|
+
def install_missing_hooks
|
|
10
|
+
return unless Dir.exist?(hooks_dir)
|
|
11
|
+
|
|
12
|
+
Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
|
|
13
|
+
hook_name = File.basename(template_path, '.sh')
|
|
14
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
15
|
+
|
|
16
|
+
next if File.exist?(hook_path)
|
|
17
|
+
|
|
18
|
+
puts "🪝 Installing #{hook_name} hook..."
|
|
19
|
+
FileUtils.cp(template_path, hook_path)
|
|
20
|
+
File.chmod(0o755, hook_path)
|
|
21
|
+
puts "✅ #{hook_name} hook installed"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def check_hook_updates
|
|
26
|
+
return puts 'ℹ️ No .git directory found' unless Dir.exist?(hooks_dir)
|
|
27
|
+
|
|
28
|
+
updates_available = false
|
|
29
|
+
|
|
30
|
+
Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
|
|
31
|
+
hook_name = File.basename(template_path, '.sh')
|
|
32
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
33
|
+
|
|
34
|
+
if File.exist?(hook_path)
|
|
35
|
+
template_content = File.read(template_path)
|
|
36
|
+
current_content = File.read(hook_path)
|
|
37
|
+
|
|
38
|
+
if template_content != current_content
|
|
39
|
+
puts "🔄 Update available for #{hook_name} hook"
|
|
40
|
+
updates_available = true
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
puts "➕ New hook template available: #{hook_name}"
|
|
44
|
+
updates_available = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if updates_available
|
|
49
|
+
puts "Run 'rake labdev:sync:hooks' to update hooks interactively"
|
|
50
|
+
else
|
|
51
|
+
puts '✅ All hooks are up to date'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def update_hooks_interactive
|
|
56
|
+
return puts 'ℹ️ No .git directory found' unless Dir.exist?(hooks_dir)
|
|
57
|
+
|
|
58
|
+
Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
|
|
59
|
+
hook_name = File.basename(template_path, '.sh')
|
|
60
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
61
|
+
|
|
62
|
+
if File.exist?(hook_path)
|
|
63
|
+
template_content = File.read(template_path)
|
|
64
|
+
current_content = File.read(hook_path)
|
|
65
|
+
|
|
66
|
+
next if template_content == current_content
|
|
67
|
+
|
|
68
|
+
puts "🔄 #{hook_name} hook has updates available"
|
|
69
|
+
puts "Current file exists at: #{hook_path}"
|
|
70
|
+
|
|
71
|
+
print "Update #{hook_name} hook? [y/N]: "
|
|
72
|
+
response = $stdin.gets.chomp.downcase
|
|
73
|
+
|
|
74
|
+
if %w[y yes].include?(response)
|
|
75
|
+
File.write(hook_path, template_content)
|
|
76
|
+
File.chmod(0o755, hook_path)
|
|
77
|
+
puts "✅ #{hook_name} hook updated"
|
|
78
|
+
else
|
|
79
|
+
puts "⏭️ Skipped #{hook_name} hook"
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
puts "➕ New hook template: #{hook_name}"
|
|
83
|
+
print "Install #{hook_name} hook? [Y/n]: "
|
|
84
|
+
response = $stdin.gets.chomp.downcase
|
|
85
|
+
|
|
86
|
+
if response != 'n' && response != 'no'
|
|
87
|
+
FileUtils.cp(template_path, hook_path)
|
|
88
|
+
File.chmod(0o755, hook_path)
|
|
89
|
+
puts "✅ #{hook_name} hook installed"
|
|
90
|
+
else
|
|
91
|
+
puts "⏭️ Skipped #{hook_name} hook"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def list_hook_templates
|
|
98
|
+
puts '📋 Available git hook templates:'
|
|
99
|
+
puts ''
|
|
100
|
+
|
|
101
|
+
Dir.glob("#{hooks_template_dir}/*.sh").each do |template_path|
|
|
102
|
+
hook_name = File.basename(template_path, '.sh')
|
|
103
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
104
|
+
|
|
105
|
+
status = nil
|
|
106
|
+
if File.exist?(hook_path)
|
|
107
|
+
template_content = File.read(template_path)
|
|
108
|
+
current_content = File.read(hook_path)
|
|
109
|
+
status = template_content == current_content ? '✅ installed' : '🔄 update available'
|
|
110
|
+
else
|
|
111
|
+
status = '➕ not installed'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
description = case hook_name
|
|
115
|
+
when 'pre-commit'
|
|
116
|
+
'Advisory checks & syntax validation (non-blocking)'
|
|
117
|
+
when 'pre-push'
|
|
118
|
+
'Comprehensive linting & quality gate (blocking)'
|
|
119
|
+
else
|
|
120
|
+
''
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
puts " #{hook_name}: #{status}"
|
|
124
|
+
puts " #{description}" unless description.empty?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def hooks_template_dir
|
|
131
|
+
HOOKS_SOURCE_DIR
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def hooks_dir
|
|
135
|
+
HOOKS_DIR
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module DocOpsLab
|
|
7
|
+
module Dev
|
|
8
|
+
module Help
|
|
9
|
+
class << self
|
|
10
|
+
def show_task_help task_string=nil
|
|
11
|
+
tasks_def_path = File.join(GEM_ROOT, 'specs/data/tasks-def.yml')
|
|
12
|
+
|
|
13
|
+
unless File.exist?(tasks_def_path)
|
|
14
|
+
puts '❌ Tasks definition file not found'
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
tasks_def = YAML.load_file(tasks_def_path)
|
|
19
|
+
|
|
20
|
+
if task_string.nil?
|
|
21
|
+
show_general_help
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Normalize task string (allow with or without labdev: prefix)
|
|
26
|
+
task_string = "labdev:#{task_string}" unless task_string.start_with?('labdev:')
|
|
27
|
+
|
|
28
|
+
# Parse task path
|
|
29
|
+
task_parts = task_string.sub('labdev:', '').split(':')
|
|
30
|
+
|
|
31
|
+
# Navigate to task in YAML structure
|
|
32
|
+
current = tasks_def['labdev']
|
|
33
|
+
found = true
|
|
34
|
+
task_parts.each do |part|
|
|
35
|
+
if current.is_a?(Hash) && current[part]
|
|
36
|
+
current = current[part]
|
|
37
|
+
else
|
|
38
|
+
puts "❌ Task not found: #{task_string}"
|
|
39
|
+
found = false
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return unless found
|
|
45
|
+
|
|
46
|
+
show_task_details(task_string, current)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def show_general_help
|
|
52
|
+
puts '📚 DocOps Lab Development Tools - Available Tasks'
|
|
53
|
+
puts '=' * 60
|
|
54
|
+
puts ''
|
|
55
|
+
puts 'Use `bundle exec rake -T | grep labdev:` to see all tasks.'
|
|
56
|
+
puts 'Use `bundle exec rake labdev:help[verb:subtask]` for detailed help.'
|
|
57
|
+
puts ''
|
|
58
|
+
puts "Example: bundle exec rake 'labdev:help[run:script]'"
|
|
59
|
+
puts ''
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show_task_details task_string, task_info
|
|
63
|
+
puts "📚 Help for: #{task_string}"
|
|
64
|
+
puts '=' * 60
|
|
65
|
+
puts ''
|
|
66
|
+
|
|
67
|
+
if task_info['_desc']
|
|
68
|
+
puts "Description: #{task_info['_desc']}"
|
|
69
|
+
puts ''
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if task_info['_docs']
|
|
73
|
+
puts 'Documentation:'
|
|
74
|
+
puts task_info['_docs']
|
|
75
|
+
puts ''
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if task_info['_alias']
|
|
79
|
+
puts "⚠️ This is an alias for: #{task_info['_alias']}"
|
|
80
|
+
# Display help for the aliased task
|
|
81
|
+
show_task_help(task_info['_alias'])
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Show subtasks if this is a namespace
|
|
86
|
+
subtasks = task_info.select { |k, v| !k.start_with?('_') && v.is_a?(Hash) }
|
|
87
|
+
if subtasks.any?
|
|
88
|
+
puts 'Available subtasks:'
|
|
89
|
+
puts ''
|
|
90
|
+
subtasks.each do |subtask_name, subtask_info|
|
|
91
|
+
desc = subtask_info['_desc'] || '(no description)'
|
|
92
|
+
alias_note = subtask_info['_alias'] ? " → #{subtask_info['_alias']}" : ''
|
|
93
|
+
puts " #{task_string}:#{subtask_name}#{alias_note}"
|
|
94
|
+
puts " #{desc}"
|
|
95
|
+
puts ''
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if task_info['_args']
|
|
100
|
+
puts 'Arguments:'
|
|
101
|
+
task_info['_args'].each do |arg_name, arg_info|
|
|
102
|
+
required = arg_info['required'] ? '(required)' : '(optional)'
|
|
103
|
+
puts " #{arg_name} #{required}"
|
|
104
|
+
puts " #{arg_info['summ']}" if arg_info['summ']
|
|
105
|
+
puts " #{arg_info['docs'].strip.split("\n").join("\n ")}" if arg_info['docs']
|
|
106
|
+
puts ''
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return unless task_info['_test']
|
|
111
|
+
|
|
112
|
+
puts 'Example usage:'
|
|
113
|
+
task_info['_test'].each do |test_cmd|
|
|
114
|
+
puts " #{test_cmd}"
|
|
115
|
+
end
|
|
116
|
+
puts ''
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|