docopslab-dev 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8417e50ad63a4c8d98af45af088b4088b7fb75106d2997da327c51d2d106f5d
4
- data.tar.gz: 0be1d194d518f593d9523b7bd446113a9a140f5861bf538bc38791076a3616cf
3
+ metadata.gz: 644c00380af7862d5e02eb3c14ec8b1ec824612440cbb292fd9f9cb4f3900a72
4
+ data.tar.gz: 85187c04d2248a6360a03e82210d5f7c26974f3b15b6a467feef91cd457d1d0d
5
5
  SHA512:
6
- metadata.gz: 139611f20da4c0fbe7a68b497c930989e4fbb7019633d627e3039248862a25c8381a4257b3be88edbeebaba29578af9e3e642a261250c5d179c632982f58b204
7
- data.tar.gz: 786b54d7f24f35f80d0483ad5692b54ce737e7cc92d788ff2f959452789e8706c54a11b55979a0866ab64c35be1fbd78acc7b37065341eab2184f85cc4969fc1
6
+ metadata.gz: 80b9f8f128d8d0c27c30a427105fd6f88ed64fc128e2db25ee20ef5ae83df006e80c345984306ab37ea3fc37cccaabea3ff0fae685beb3168df7790824a30362
7
+ data.tar.gz: 05acd13909ec5358af9f34b20620fc3d9e15d3a0a4b9301df229481aa993f0051ad53288ad5347cbb382d91b44a102d528132699160102f60dd6c4a2c9cf18e6
data/README.adoc CHANGED
@@ -525,6 +525,34 @@ For full Vale configuration settings ("`keys`") reference, see the link:https://
525
525
 
526
526
  // end::config-vale[]
527
527
 
528
+ [[git-lint]]
529
+ === git-lint
530
+
531
+ Git commit message checking for local hooks.
532
+
533
+ Convention data:: `.config/.vendor/docopslab/commit-conventions.yml`
534
+ Project convention overrides:: `.config/commit-conventions.yml` (inherits via `inherit_from`)
535
+ Base config:: `.config/.vendor/docopslab/git-lint.yml`
536
+ Project config overrides:: `.config/git-lint.local.yml`
537
+ Ephemeral config:: `.config/git-lint.yml`
538
+ Sync command:: `bundle exec rake labdev:sync:configs`
539
+ Branch lint command:: `bundle exec rake labdev:lint:commits`
540
+ Commit-message hook command:: `bundle exec rake labdev:lint:commit[path/to/COMMIT_EDITMSG]`
541
+
542
+ The generated git-lint config is derived from shared commit convention data so other commit-message linters can reuse the same types, scopes, and prose rules later.
543
+ Commit subjects must follow `<type>[optional scope]: Subject`, use a configured type and scope, start the subject with a capitalized imperative verb, and omit terminal punctuation.
544
+
545
+ Commit convention overrides are mapping-based.
546
+ Local projects can add or overwrite keys under `rules`, `conventions.types`, and `conventions.scopes`.
547
+ Set a key to `null` in the local file to drop an inherited convention.
548
+
549
+ Version 6.x of `git-lint` expects an XDG config path named `git-lint/configuration.yml`.
550
+ The `labdev` wrappers bridge `.config/git-lint.yml` into that path at runtime, so use the rake tasks instead of invoking `git-lint` directly.
551
+
552
+ The distributed `commit-msg` hook runs `labdev:lint:commit` against the unsaved commit message.
553
+ This is a local, commit-time check by design, not a CI gate: fixing a violation caught after a push means rewriting shared history, which the local hook avoids entirely.
554
+ `labdev:lint:commits` remains available for manually linting a branch or arbitrary commit range.
555
+
528
556
  [[htmlproofer]]
529
557
  === HTMLProofer
530
558
  // tag::config-htmlproofer[]
@@ -938,6 +966,7 @@ The `gh` CLI is preferred and will be used when present.
938
966
 
939
967
  // end::reference[]
940
968
 
969
+
941
970
  // tag::workflow[]
942
971
  [[task-reference]]
943
972
  == Task Reference
@@ -1024,6 +1053,11 @@ HTMLProofer::
1024
1053
  +
1025
1054
  bundle exec htmlproofer --ignore-urls "/www.github.com/,/foo.com/" ./_site
1026
1055
 
1056
+ git-lint::
1057
+ +
1058
+ bundle exec rake "labdev:lint:commit[.git/COMMIT_EDITMSG]"
1059
+ bundle exec rake labdev:lint:commits
1060
+
1027
1061
  // end::standard-usage[]
1028
1062
  // end::usage[]
1029
1063
  // end::workflow[]
@@ -1181,10 +1215,17 @@ To build the `docopslab/dev` Docker image:
1181
1215
  [.prompt]
1182
1216
  bundle exec rake gemdo:build_docker
1183
1217
 
1218
+ [[]]
1219
+
1184
1220
  [[publish-artifacts]]
1185
1221
  === Publish & Deploy Artifacts
1186
1222
 
1187
- For general DocOps Lab release procedures, see link:{xref_docs_release_url}[Release Process (General)].
1223
+ Commit changes and push to GitHub before publishing generated artifacts.
1224
+
1225
+ [NOTE]
1226
+ The `docopslab-dev` gem does not issue official release notices or registrations on GitHub the way other DocOps Lab gems do.
1227
+
1228
+ For general DocOps Lab release procedures, see link:https://docopslab.org/docs/release/[Release Process (General)].
1188
1229
 
1189
1230
  The `docopslab-dev` gem and `docopslab/dev` Docker image follow that process with these specific considerations:
1190
1231
 
@@ -1259,4 +1300,4 @@ Documentation released under Creative Commons Attribution 4.0 International (CC
1259
1300
  === Bill of Materials
1260
1301
 
1261
1302
  No externally sourced content or code is contained in this project.
1262
- All third-party dependencies are permissively licensed and are downloaded independently, never provided by DocOps Lab.
1303
+ All third-party dependencies are permissively licensed and are downloaded independently, never provided by DocOps Lab.
@@ -32,10 +32,11 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency 'yaml', '~> 0.2'
33
33
 
34
34
  # Code quality and linting
35
- spec.add_dependency 'asciisourcerer', '~> 0.2'
35
+ spec.add_dependency 'asciisourcerer', '~> 0.5'
36
36
  spec.add_dependency 'debride', '~> 1.13'
37
37
  spec.add_dependency 'fasterer', '~> 0.11'
38
38
  spec.add_dependency 'flog', '~> 4.8'
39
+ spec.add_dependency 'git-lint', '~> 6.2'
39
40
  spec.add_dependency 'reek', '~> 6.5'
40
41
  spec.add_dependency 'rubocop', '~> 1.80'
41
42
  spec.add_dependency 'rubocop-rake', '~> 0.7'
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'yaml'
5
+
3
6
  module DocOpsLab
4
7
  module Dev
5
8
  module ConfigManager
@@ -55,6 +58,51 @@ module DocOpsLab
55
58
  end
56
59
  end
57
60
 
61
+ def generate_git_lint_config _context
62
+ default_config = load_git_lint_defaults
63
+ return false unless default_config
64
+
65
+ base_config = File.join(Paths.config_vendor_dir, 'git-lint.yml')
66
+ project_config = '.config/git-lint.local.yml'
67
+ generated_config = Paths::CONFIG_FILES[:git_lint]
68
+
69
+ merged_content = default_config
70
+ if File.exist?(base_config)
71
+ merged_content = deep_merge_configs(merged_content, YAML.load_file(base_config) || {})
72
+ end
73
+ if File.exist?(project_config)
74
+ merged_content = deep_merge_configs(merged_content, YAML.load_file(project_config) || {})
75
+ end
76
+
77
+ conventions = load_commit_conventions
78
+ merged_content = apply_commit_conventions_to_git_lint(merged_content, conventions) if conventions
79
+
80
+ rendered = YAML.dump(merged_content)
81
+ FileUtils.mkdir_p(File.dirname(generated_config))
82
+
83
+ if !File.exist?(generated_config) || File.read(generated_config) != rendered
84
+ File.write(generated_config, rendered)
85
+ puts " 📝 Generated #{generated_config} from git-lint defaults + DocOps Lab conventions"
86
+ true
87
+ else
88
+ false
89
+ end
90
+ end
91
+
92
+ def load_commit_conventions
93
+ base_path = File.join(Paths.config_vendor_dir, 'commit-conventions.yml')
94
+ local_path = '.config/commit-conventions.yml'
95
+
96
+ if File.exist?(local_path)
97
+ load_inheritable_yaml_config(local_path)
98
+ elsif File.exist?(base_path)
99
+ YAML.load_file(base_path) || {}
100
+ end
101
+ rescue StandardError => e
102
+ warn "⚠️ Failed to load commit conventions: #{e.message}"
103
+ nil
104
+ end
105
+
58
106
  def load_htmlproofer_config config_path=nil, policy: 'merge'
59
107
  config_paths = if config_path && File.exist?(config_path)
60
108
  [config_path]
@@ -99,6 +147,67 @@ module DocOpsLab
99
147
  config.transform_keys(&:to_sym)
100
148
  end
101
149
 
150
+ def load_git_lint_defaults
151
+ spec = Gem.loaded_specs['git-lint'] || Gem::Specification.find_all_by_name('git-lint').first
152
+ unless spec
153
+ warn "⚠️ git-lint is not installed. Run 'bundle install'."
154
+ return nil
155
+ end
156
+
157
+ YAML.load_file(File.join(spec.full_gem_path, 'lib/git/lint/configuration/defaults.yml')) || {}
158
+ rescue StandardError => e
159
+ warn "⚠️ Failed to load git-lint defaults: #{e.message}"
160
+ nil
161
+ end
162
+
163
+ def apply_commit_conventions_to_git_lint config, conventions
164
+ types = convention_slugs(conventions, 'types')
165
+ scopes = convention_slugs(conventions, 'scopes')
166
+ separator = conventions.dig('rules', 'subject', 'allowed_scope_separator') || '+'
167
+ max_length = conventions.dig('rules', 'subject', 'maximum')
168
+ body_max = conventions.dig('rules', 'body', 'maximum_line_length')
169
+
170
+ subject = config.dig('commits', 'subject')
171
+ if subject && types.any?
172
+ scope_pattern = git_lint_scope_pattern(scopes, separator)
173
+ subject['prefix']['includes'] = types.sort.map { |type| "#{Regexp.escape(type)}#{scope_pattern}: " }
174
+ end
175
+
176
+ subject['length']['maximum'] = max_length if subject && max_length
177
+ body = config.dig('commits', 'body')
178
+ body['line_length']['maximum'] = body_max if body&.dig('line_length') && body_max
179
+
180
+ config
181
+ end
182
+
183
+ def git_lint_scope_pattern scopes, separator
184
+ return '(?:\\([a-z0-9_-]+\\))?' if scopes.empty?
185
+
186
+ escaped_scopes = scopes.sort.map { |scope| Regexp.escape(scope) }
187
+ escaped_separator = Regexp.escape(separator)
188
+ "(?:\\((?:#{escaped_scopes.join('|')})(?:#{escaped_separator}(?:#{escaped_scopes.join('|')}))*\\))?"
189
+ end
190
+
191
+ def convention_slugs conventions, key
192
+ values = conventions.dig('conventions', key)
193
+ case values
194
+ when Hash
195
+ values.keys
196
+ else
197
+ Array(values).filter_map { |entry| entry['slug'] if entry.is_a?(Hash) }
198
+ end
199
+ end
200
+
201
+ def load_inheritable_yaml_config path
202
+ local = YAML.load_file(path) || {}
203
+ inherit_from = local.delete('inherit_from')
204
+ return local unless inherit_from
205
+
206
+ base_path = File.expand_path(inherit_from, File.dirname(path))
207
+ base = File.exist?(base_path) ? YAML.load_file(base_path) || {} : {}
208
+ deep_merge_configs(base, local)
209
+ end
210
+
102
211
  def merge_yaml_configs base_path, local_path
103
212
  # Implement RuboCop-style inheritance for YAML files
104
213
  require 'yaml'
@@ -119,6 +119,8 @@ module DocOpsLab
119
119
  description = case hook_name
120
120
  when 'pre-commit'
121
121
  'Advisory checks & syntax validation (non-blocking)'
122
+ when 'commit-msg'
123
+ 'Commit message style gate (blocking)'
122
124
  when 'pre-push'
123
125
  'Comprehensive linting & quality gate (blocking)'
124
126
  else
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'fileutils'
4
5
  require 'pathname'
5
6
  require 'sourcerer/util/pathifier'
7
+ require 'tmpdir'
6
8
 
7
9
  module DocOpsLab
8
10
  module Dev
@@ -185,6 +187,174 @@ module DocOpsLab
185
187
  success
186
188
  end
187
189
 
190
+ def run_git_lint context, target=nil, opts_string=''
191
+ puts '🧾 Running git-lint...'
192
+ context.generate_git_lint_config if context.respond_to?(:generate_git_lint_config)
193
+
194
+ unless File.exist?(Paths::CONFIG_FILES[:git_lint])
195
+ puts "❌ No git-lint config found. Run 'labdev:sync:configs' to create one."
196
+ return false
197
+ end
198
+
199
+ cmd = %w[bundle exec git-lint analyze]
200
+ if target
201
+ commits = target.split(',').reject { |sha| merge_commit?(sha) }
202
+ if commits.empty?
203
+ puts 'ℹ️ All targeted commits are merge commits (exempt from subject conventions); nothing to check.'
204
+ return true
205
+ end
206
+ cmd += ['--commit', commits.join(',')]
207
+ else
208
+ cmd << '--branch'
209
+ end
210
+ cmd += opts_string.split unless opts_string.empty?
211
+
212
+ success = with_git_lint_config_env do |env|
213
+ system(env, *cmd)
214
+ end
215
+
216
+ if success
217
+ puts '✅ git-lint passed'
218
+ else
219
+ puts '❌ git-lint found commit message issues'
220
+ end
221
+
222
+ success
223
+ end
224
+
225
+ def merge_commit_message? message_file
226
+ subject = File.readlines(message_file, chomp: true).find { |line| !line.strip.empty? }
227
+ return false if subject.nil?
228
+
229
+ subject.match?(/\AMerge /)
230
+ end
231
+
232
+ def merge_commit? sha
233
+ out, status = Open3.capture2('git', 'rev-list', '--no-walk', '--parents', sha)
234
+ return false unless status.success?
235
+
236
+ # First token is the commit itself; anything beyond one remaining token is a second parent.
237
+ out.strip.split.size > 2
238
+ end
239
+
240
+ def run_git_lint_hook context, message_file
241
+ puts "🧾 Checking commit message: #{message_file}"
242
+ context.generate_git_lint_config if context.respond_to?(:generate_git_lint_config)
243
+
244
+ unless File.exist?(message_file)
245
+ puts "❌ Commit message file not found: #{message_file}"
246
+ return false
247
+ end
248
+
249
+ if merge_commit_message?(message_file)
250
+ puts 'ℹ️ Merge commit message (exempt from subject conventions); nothing to check.'
251
+ return true
252
+ end
253
+
254
+ convention_success = validate_commit_message_subject(context, message_file)
255
+ git_lint_success = with_git_lint_config_env do |env|
256
+ system(env, 'bundle', 'exec', 'git-lint', '--hook', message_file)
257
+ end
258
+
259
+ if convention_success && git_lint_success
260
+ puts '✅ Commit message passed'
261
+ true
262
+ else
263
+ puts '❌ Commit message failed'
264
+ false
265
+ end
266
+ end
267
+
268
+ def validate_commit_message_subject context, message_file
269
+ subject = File.readlines(message_file, chomp: true).find { |line| !line.strip.empty? }
270
+ return true if subject.nil? || subject.match?(/\A(?:fixup|squash|amend)!\s/)
271
+
272
+ conventions = context.load_commit_conventions if context.respond_to?(:load_commit_conventions)
273
+ return true unless conventions
274
+
275
+ types = convention_slugs(conventions, 'types').sort
276
+ scopes = convention_slugs(conventions, 'scopes').sort
277
+ separator = conventions.dig('rules', 'subject', 'allowed_scope_separator') || '+'
278
+ reject_punctuation = conventions.dig('rules', 'subject', 'reject_terminal_punctuation')
279
+ reject_agent_signature = conventions.dig('rules', 'text', 'reject_agent_signature')
280
+
281
+ match = subject.match(/\A(?<type>[a-z]+)(?:\((?<scope>[^)]+)\))?: (?<subject>.+)\z/)
282
+ unless match
283
+ puts '❌ Commit subject must match `<type>[optional scope]: Subject`.'
284
+ return false
285
+ end
286
+
287
+ valid = true
288
+ valid = false unless validate_commit_type(match[:type], types)
289
+ valid = false unless validate_commit_scopes(match[:scope], scopes, separator)
290
+ valid = false unless validate_subject_text(match[:subject], reject_punctuation)
291
+ valid = false if reject_agent_signature && agent_signature?(File.read(message_file))
292
+ valid
293
+ end
294
+
295
+ def with_git_lint_config_env
296
+ Dir.mktmpdir('docopslab-dev-git-lint') do |dir|
297
+ config_dir = File.join(dir, 'git-lint')
298
+ FileUtils.mkdir_p(config_dir)
299
+ FileUtils.cp(Paths::CONFIG_FILES[:git_lint], File.join(config_dir, 'configuration.yml'))
300
+ yield({ 'XDG_CONFIG_HOME' => dir })
301
+ end
302
+ end
303
+
304
+ def convention_slugs conventions, key
305
+ values = conventions.dig('conventions', key)
306
+ case values
307
+ when Hash
308
+ values.keys
309
+ else
310
+ Array(values).filter_map { |entry| entry['slug'] if entry.is_a?(Hash) }
311
+ end
312
+ end
313
+
314
+ def validate_commit_type type, types
315
+ return true if types.include?(type)
316
+
317
+ puts "❌ Commit type `#{type}` is not allowed. Use one of: #{types.join(', ')}."
318
+ false
319
+ end
320
+
321
+ def validate_commit_scopes scope_text, scopes, separator
322
+ return true if scope_text.nil? || scopes.empty?
323
+
324
+ invalid = scope_text.split(separator).reject { |scope| scopes.include?(scope) }
325
+ return true if invalid.empty?
326
+
327
+ puts "❌ Commit scope(s) not allowed: #{invalid.join(', ')}."
328
+ puts " Use configured scopes joined with `#{separator}` when multiple scopes are needed."
329
+ false
330
+ end
331
+
332
+ def validate_subject_text subject_text, reject_punctuation
333
+ valid = true
334
+ unless subject_text.match?(/\A[[:upper:]]/)
335
+ puts '❌ Commit subject must start with a capitalized imperative verb.'
336
+ valid = false
337
+ end
338
+
339
+ if reject_punctuation && subject_text.match?(/[.?!]\z/)
340
+ puts '❌ Commit subject must not end with terminal punctuation.'
341
+ valid = false
342
+ end
343
+
344
+ valid
345
+ end
346
+
347
+ def agent_signature? message
348
+ patterns = [
349
+ /Co-authored-by:\s*(?:Codex|Claude|ChatGPT)/i,
350
+ /Generated with (?:Claude Code|Codex|ChatGPT)/i
351
+ ]
352
+ return false unless patterns.any? { |pattern| message.match?(pattern) }
353
+
354
+ puts '❌ Commit message must not include agent authorship signatures.'
355
+ true
356
+ end
357
+
188
358
  def run_vale context, file_path=nil, opts_string='', output_format: :cli, filter: nil, style_override: nil
189
359
  scope = file_path ? :file : :project
190
360
  running_on = file_path ? "file: #{file_path}" : scope.to_s
@@ -18,6 +18,7 @@ module DocOpsLab
18
18
  CONFIG_FILES = {
19
19
  vale: '.config/vale.ini',
20
20
  htmlproofer: '.config/htmlproofer.yml',
21
+ git_lint: '.config/git-lint.yml',
21
22
  rubocop: '.config/rubocop.yml'
22
23
  }.freeze
23
24
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'pathname'
4
5
  require 'asciisourcerer'
5
6
  require 'sourcerer/util/pathifier'
6
7
 
@@ -28,17 +29,24 @@ module DocOpsLab
28
29
  run_with_format(path, exts: MD_EXTS, form: form, syntax: syntax, overlay: true)
29
30
  end
30
31
 
32
+ # Skim a Ruby application for its API surface (Modules, Classes, Methods, etc.). Form and syntax are auto-detected.
33
+ def run_ruby path, form: nil, syntax: nil
34
+ # path is path + lib/
35
+ path = File.join(path, 'lib')
36
+ run_with_format(path, exts: ['.rb'], form: form, syntax: syntax)
37
+ end
38
+
31
39
  private
32
40
 
33
- def run_with_format path, exts:, form: nil, syntax: nil, default_forms: nil, overlay: false
41
+ def run_with_format path, exts:, form: nil, syntax: nil, **opts
34
42
  unless path
35
43
  puts '❌ Path is required.'
36
- puts 'Usage: bundle exec rake labdev:skim[path,form,syntax]'
44
+ puts 'Usage: bundle exec rake labdev:skim:<type>[path,form,syntax]'
37
45
  return
38
46
  end
39
47
 
40
- forms = form ? parse_forms(form) : default_forms
41
- file_paths = overlay ? resolve_overlay_paths(path, exts) : resolve_paths(path, exts)
48
+ forms = form ? parse_forms(form) : opts[:default_forms]
49
+ file_paths = opts[:overlay] ? resolve_overlay_paths(path, exts) : resolve_paths(path, exts)
42
50
 
43
51
  if file_paths.empty?
44
52
  ext_desc = exts.size == 1 ? exts.first : exts.join(', ')
@@ -51,7 +59,9 @@ module DocOpsLab
51
59
  file_paths.each do |fp|
52
60
  skim_opts = { categories: cats }
53
61
  skim_opts[:forms] = forms if forms
54
- results[fp] = Sourcerer::SourceSkim.skim_file(fp, **skim_opts)
62
+ skim_opts[:descriptions] = true if opts[:descriptions]
63
+
64
+ results[relative_path(fp)] = Sourcerer::SourceSkim.skim_file(fp, **skim_opts)
55
65
  end
56
66
  portable = JSON.parse(JSON.generate(results))
57
67
 
@@ -85,6 +95,10 @@ module DocOpsLab
85
95
  end
86
96
  end
87
97
 
98
+ def relative_path file_path
99
+ Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
100
+ end
101
+
88
102
  def parse_forms form
89
103
  form.split(',').map { |f| f.strip.to_sym }
90
104
  end
@@ -98,7 +112,7 @@ module DocOpsLab
98
112
  return :yaml unless form
99
113
 
100
114
  syntax = 'yaml' if syntax == 'yml'
101
- puts
115
+
102
116
  return syntax.to_sym if syntax
103
117
 
104
118
  :json
@@ -257,6 +257,21 @@ module DocOpsLab
257
257
  synced_count.positive? || skipped_count.positive?
258
258
  end
259
259
 
260
+ def ensure_library_available
261
+ Library.ensure_available!
262
+ true
263
+ rescue StandardError => e
264
+ puts "❌ #{e.message}"
265
+ false
266
+ end
267
+
268
+ def load_manifest_or_report_error
269
+ YAML.load_file(MANIFEST_PATH)
270
+ rescue StandardError => e
271
+ puts "❌ Failed to parse #{MANIFEST_PATH}: #{e.message}"
272
+ nil
273
+ end
274
+
260
275
  def sync_config_files context, tool_filter: :all, offline: false
261
276
  # Validate tool filter parameter
262
277
  unless tool_filter == :all || tool_filter.is_a?(String) || tool_filter.is_a?(Symbol)
@@ -273,13 +288,10 @@ module DocOpsLab
273
288
  return false
274
289
  end
275
290
 
276
- # Parse manifest
277
- begin
278
- manifest = YAML.load_file(MANIFEST_PATH)
279
- rescue StandardError => e
280
- puts "❌ Failed to parse #{MANIFEST_PATH}: #{e.message}"
281
- return false
282
- end
291
+ manifest = load_manifest_or_report_error
292
+ return false unless manifest
293
+
294
+ return false unless offline || ensure_library_available
283
295
 
284
296
  config_packs_root = Library.resolve('config-packs')
285
297
  unless config_packs_root && Dir.exist?(config_packs_root)
@@ -383,6 +395,7 @@ module DocOpsLab
383
395
  generated_count = 0
384
396
  generated_count += 1 if context.generate_vale_config
385
397
  generated_count += 1 if context.generate_htmlproofer_config
398
+ generated_count += 1 if context.generate_git_lint_config
386
399
 
387
400
  puts ' ✅ All runtime configs up to date' if generated_count.zero?
388
401
 
@@ -146,6 +146,12 @@ module DocOpsLab
146
146
  opts = args[:opts] || ''
147
147
  Dev.run_actionlint(opts)
148
148
  end
149
+
150
+ desc desc_for('run:git_lint')
151
+ task :git_lint, [:opts] => [] do |_t, args|
152
+ opts = args[:opts] || ''
153
+ Dev.run_git_lint(nil, opts)
154
+ end
149
155
  end
150
156
 
151
157
  # ============================================================
@@ -339,6 +345,29 @@ module DocOpsLab
339
345
  end
340
346
  end
341
347
 
348
+ desc desc_for('lint:commits')
349
+ task :commits, %i[target opts] => [] do |_t, args|
350
+ success = Dev.run_git_lint(args[:target], args[:opts] || '')
351
+ exit(1) unless success
352
+ end
353
+
354
+ desc desc_for('lint:commit')
355
+ task :commit, [:path] => [] do |_t, args|
356
+ unless args[:path]
357
+ puts 'Usage: bundle exec rake labdev:lint:commit[path/to/COMMIT_EDITMSG]'
358
+ exit(1)
359
+ end
360
+
361
+ success = Dev.run_git_lint_hook(args[:path])
362
+ exit(1) unless success
363
+ end
364
+
365
+ desc 'Deprecated alias for labdev:lint:commit'
366
+ task :commit_msg, [:path] => [] do |_t, args|
367
+ warn '⚠️ labdev:lint:commit_msg is deprecated; use labdev:lint:commit.'
368
+ Rake::Task['labdev:lint:commit'].invoke(args[:path])
369
+ end
370
+
342
371
  desc desc_for('lint:spellcheck')
343
372
  task :spellcheck, %i[path opts] => [] do |_t, args|
344
373
  path = args[:path]
@@ -390,14 +419,12 @@ module DocOpsLab
390
419
  end
391
420
 
392
421
  desc desc_for('heal:adoc')
393
- # Add an optional path argument that defaults to nil
394
422
  task :adoc, %i[path] => [] do |_t, args|
395
423
  Dev.run_adoc_auto_fix(args[:path])
396
424
  end
397
425
 
398
426
  desc desc_for('heal:all')
399
427
  task :all do
400
- # if the user passed an argument, we wan to tell them this task does not accept any arguments and we want to peaec out of this operation rather than running it
401
428
  if ARGV.any? { |arg| arg.include?('labdev:heal:all') && arg.include?('[') }
402
429
  puts '⚠️ labdev:heal:all does not accept any arguments. Exiting.'
403
430
  puts 'Use labdev:heal:ruby[path] or labdev:heal:adoc[path] to auto-fix specific files.'
@@ -431,6 +458,11 @@ module DocOpsLab
431
458
  task :md, %i[path form syntax] => [] do |_t, args|
432
459
  Skim.run_md(args[:path], form: args[:form], syntax: args[:syntax])
433
460
  end
461
+
462
+ desc desc_for('skim:ruby')
463
+ task :ruby, %i[path form syntax] => [] do |_t, args|
464
+ Skim.run_ruby(args[:path], form: args[:form], syntax: args[:syntax])
465
+ end
434
466
  end
435
467
 
436
468
  # ============================================================
@@ -58,7 +58,7 @@ module DocOpsLab
58
58
  # Run command in docopslab/dev container
59
59
  # Handle both String and Array command formats
60
60
  cmd_str = command.is_a?(Array) ? command.shelljoin : command
61
- docker_cmd = "docker run -it --rm -v \"$(pwd):/workspace\" -w /workspace docopslab/dev #{cmd_str}"
61
+ docker_cmd = "docker run --rm -v \"$(pwd):/workspace\" -w /workspace docopslab/dev #{cmd_str}"
62
62
  puts "🐳 Running in Docker: #{cmd_str}"
63
63
  system(docker_cmd)
64
64
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module DocOpsLab
4
4
  module Dev
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  RUBY_TARGET = '3.2.7'
7
7
  end
8
8
  end
data/lib/docopslab/dev.rb CHANGED
@@ -313,6 +313,14 @@ module DocOpsLab
313
313
  ConfigManager.generate_htmlproofer_config(self)
314
314
  end
315
315
 
316
+ def generate_git_lint_config
317
+ ConfigManager.generate_git_lint_config(self)
318
+ end
319
+
320
+ def load_commit_conventions
321
+ ConfigManager.load_commit_conventions
322
+ end
323
+
316
324
  def load_htmlproofer_config
317
325
  ConfigManager.load_htmlproofer_config
318
326
  end
@@ -335,13 +343,21 @@ module DocOpsLab
335
343
  Linters.run_actionlint(self, opts_string)
336
344
  end
337
345
 
346
+ def run_git_lint target=nil, opts_string=''
347
+ Linters.run_git_lint(self, target, opts_string)
348
+ end
349
+
350
+ def run_git_lint_hook message_file
351
+ Linters.run_git_lint_hook(self, message_file)
352
+ end
353
+
338
354
  def run_all_linters
339
355
  Linters.run_all_linters(self)
340
356
  end
341
357
 
342
358
  def run_auto_fix
343
- Linters.run_auto_fix
344
- AsciiidocAutoFix.fix_asciidoc_files(self)
359
+ Linters.run_auto_fix(self)
360
+ AutoFixAsciidoc.fix_asciidoc_files(self)
345
361
  end
346
362
 
347
363
  def run_rubocop_auto_fix path: nil
@@ -20,6 +20,8 @@ templates:
20
20
  target: .gitignore
21
21
  - source: templates/README.asciidoc
22
22
  target: README.adoc
23
+ - source: templates/CLAUDE.md
24
+ target: CLAUDE.md
23
25
 
24
26
  tools:
25
27
  - tool: rubocop
@@ -75,8 +77,23 @@ tools:
75
77
  target: .config/actionlint.yml
76
78
  synced: false
77
79
 
80
+ - tool: git-lint
81
+ files:
82
+ - source: commit-conventions/base.yml
83
+ target: .config/.vendor/docopslab/commit-conventions.yml
84
+ synced: true
85
+ - source: commit-conventions/project.yml
86
+ target: .config/commit-conventions.yml
87
+ synced: false
88
+ - source: git-lint/base.yml
89
+ target: .config/.vendor/docopslab/git-lint.yml
90
+ synced: true
91
+ - source: git-lint/project.yml
92
+ target: .config/git-lint.local.yml
93
+ synced: false
94
+
78
95
  library:
79
96
  enabled: true
80
97
  source:
81
98
  repo: DocOps/lab
82
- ref: labdev-library
99
+ ref: labdev-library
@@ -113,6 +113,14 @@ labdev:
113
113
  _test:
114
114
  - bundle exec rake 'labdev:run:actionlint'
115
115
  - bundle exec rake 'labdev:run:actionlint["-oneline -verbose"]'
116
+ git_lint:
117
+ _desc: Run the base git-lint branch analyzer and options.
118
+ _args:
119
+ opts:
120
+ summ: Additional arguments to pass to the git-lint command.
121
+ required: false
122
+ _test:
123
+ - bundle exec rake 'labdev:run:git_lint'
116
124
  sync:
117
125
  _desc: Sync all managed files (configs, scripts, docs, styles, etc)
118
126
  all:
@@ -270,6 +278,23 @@ labdev:
270
278
  opts:
271
279
  summ: Additional options to pass to actionlint
272
280
  required: false
281
+ commits:
282
+ _desc: Run git-lint on current branch commits or specific commit SHAs
283
+ _args:
284
+ target:
285
+ summ: Optional commit SHA or comma-separated commit SHAs to lint
286
+ required: false
287
+ opts:
288
+ summ: Additional options to pass to git-lint
289
+ required: false
290
+ commit:
291
+ _desc: Run git-lint on an unsaved commit message file
292
+ _args:
293
+ path:
294
+ summ: Path to the commit message file passed by the commit-msg hook
295
+ required: true
296
+ commit_msg:
297
+ _alias: "labdev:lint:commit"
273
298
  spellcheck:
274
299
  _desc: Generate spellcheck report on content files
275
300
  _args:
@@ -337,7 +362,7 @@ labdev:
337
362
  - bundle exec rake 'labdev:show:rule[rubocop,Layout/LineLength]'
338
363
  - bundle exec rake 'labdev:show:rule[vale,DocOpsLab-Authoring.ExNotEg]'
339
364
  skim:
340
- _desc: Skim source files for machine-oriented structured outlines (AsciiDoc + Markdown autodetect)
365
+ _desc: Skim source files for machine-oriented structured outlines (AsciiDoc, Markdown, Ruby)
341
366
  _args:
342
367
  path:
343
368
  summ: |
@@ -402,7 +427,8 @@ labdev:
402
427
  form:
403
428
  summ: |
404
429
  Output shape: tree, flat, or tree,flat (comma-separated).
405
- Defaults to flat for Markdown. When form is explicitly passed, output defaults to JSON.
430
+ Defaults to flat for Markdown.
431
+ When form is explicitly passed, output defaults to JSON.
406
432
  required: false
407
433
  syntax:
408
434
  summ: |
@@ -413,6 +439,23 @@ labdev:
413
439
  - bundle exec rake 'labdev:skim:md[.agent/docs/]'
414
440
  - bundle exec rake 'labdev:skim:md[.agent/docs/,flat,json]'
415
441
  - bundle exec rake 'labdev:skim:md[.agent/docs/:_docs/agent/,flat,json]'
442
+ ruby:
443
+ _desc: Use ri to detect API surface (Modules, Classes, Methods, etc.).
444
+ _args:
445
+ path:
446
+ summ: |
447
+ Optional path to Ruby gem directory to skim. (Default: '.')
448
+ required: false
449
+ form:
450
+ summ: |
451
+ Output shape: tree, flat, or tree,flat (comma-separated).
452
+ Defaults to tree. When form is explicitly passed, output defaults to JSON.
453
+ required: false
454
+ syntax:
455
+ summ: |
456
+ Override output format: json or yaml.
457
+ Default: yaml when form is omitted; json when form is explicitly passed.
458
+ required: false
416
459
  # ADDING:
417
460
  help:
418
461
  _desc: Show help information about labdev tasks
@@ -430,4 +473,3 @@ labdev:
430
473
  When a task_string is passed, shows all docstrings from this file for that task.
431
474
  required: false
432
475
  _test: []
433
-
data/specs/data/tools.yml CHANGED
@@ -57,4 +57,11 @@
57
57
  config:
58
58
  inheritance: native # Supports standard YAML patterns
59
59
  uses_vendor_base: true # Syncs base config to .vendor/
60
- format: YAML
60
+ format: YAML
61
+
62
+ - slug: git-lint
63
+ name: git-lint
64
+ config:
65
+ inheritance: generated_yaml # Builds .config/git-lint.yml from shared conventions
66
+ uses_vendor_base: true
67
+ format: YAML
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docopslab-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DocOps Lab
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.2'
60
+ version: '0.5'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.2'
67
+ version: '0.5'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: debride
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '4.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: git-lint
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '6.2'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '6.2'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: reek
112
126
  requirement: !ruby/object:Gem::Requirement