docopslab-dev 0.3.1 → 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 +4 -4
- data/README.adoc +43 -2
- data/docopslab-dev.gemspec +2 -1
- data/lib/docopslab/dev/config_manager.rb +109 -0
- data/lib/docopslab/dev/git_hooks.rb +2 -0
- data/lib/docopslab/dev/linters.rb +170 -0
- data/lib/docopslab/dev/paths.rb +1 -0
- data/lib/docopslab/dev/skim.rb +20 -6
- data/lib/docopslab/dev/sync_ops.rb +20 -7
- data/lib/docopslab/dev/tasks.rb +34 -2
- data/lib/docopslab/dev/tool_execution.rb +1 -1
- data/lib/docopslab/dev/version.rb +1 -1
- data/lib/docopslab/dev.rb +18 -2
- data/specs/data/default-manifest.yml +18 -1
- data/specs/data/tasks-def.yml +45 -3
- data/specs/data/tools.yml +8 -1
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 644c00380af7862d5e02eb3c14ec8b1ec824612440cbb292fd9f9cb4f3900a72
|
|
4
|
+
data.tar.gz: 85187c04d2248a6360a03e82210d5f7c26974f3b15b6a467feef91cd457d1d0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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.
|
data/docopslab-dev.gemspec
CHANGED
|
@@ -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.
|
|
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
|
data/lib/docopslab/dev/paths.rb
CHANGED
data/lib/docopslab/dev/skim.rb
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
data/lib/docopslab/dev/tasks.rb
CHANGED
|
@@ -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
|
|
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
|
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
|
-
|
|
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
|
data/specs/data/tasks-def.yml
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|