docopslab-dev 0.1.0 → 0.3.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +682 -324
  3. data/docopslab-dev.gemspec +3 -4
  4. data/lib/docopslab/dev/cast_ops.rb +199 -0
  5. data/lib/docopslab/dev/config_manager.rb +6 -6
  6. data/lib/docopslab/dev/data_utils.rb +42 -0
  7. data/lib/docopslab/dev/docker_aware.rb +40 -0
  8. data/lib/docopslab/dev/file_utils.rb +18 -7
  9. data/lib/docopslab/dev/git_branch.rb +201 -0
  10. data/lib/docopslab/dev/git_hooks.rb +17 -11
  11. data/lib/docopslab/dev/initializer.rb +34 -11
  12. data/lib/docopslab/dev/library/cache.rb +167 -0
  13. data/lib/docopslab/dev/library/fetch.rb +209 -0
  14. data/lib/docopslab/dev/library.rb +341 -0
  15. data/lib/docopslab/dev/linters.rb +73 -15
  16. data/lib/docopslab/dev/manifest.rb +28 -0
  17. data/lib/docopslab/dev/paths.rb +0 -17
  18. data/lib/docopslab/dev/script_manager.rb +12 -6
  19. data/lib/docopslab/dev/skim.rb +109 -0
  20. data/lib/docopslab/dev/spell_check.rb +2 -2
  21. data/lib/docopslab/dev/sync_ops.rb +94 -33
  22. data/lib/docopslab/dev/tasks.rb +58 -18
  23. data/lib/docopslab/dev/version.rb +1 -1
  24. data/lib/docopslab/dev.rb +77 -36
  25. data/specs/data/default-manifest.yml +23 -5
  26. data/specs/data/library-index.yml +22 -0
  27. data/specs/data/manifest-schema.yaml +142 -4
  28. data/specs/data/tasks-def.yml +122 -10
  29. metadata +28 -73
  30. data/assets/config-packs/actionlint/base.yml +0 -13
  31. data/assets/config-packs/actionlint/project.yml +0 -13
  32. data/assets/config-packs/htmlproofer/base.yml +0 -27
  33. data/assets/config-packs/htmlproofer/project.yml +0 -25
  34. data/assets/config-packs/rubocop/base.yml +0 -130
  35. data/assets/config-packs/rubocop/project.yml +0 -8
  36. data/assets/config-packs/shellcheck/base.shellcheckrc +0 -14
  37. data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +0 -11
  38. data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +0 -8
  39. data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +0 -7
  40. data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +0 -8
  41. data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +0 -8
  42. data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +0 -8
  43. data/assets/config-packs/vale/asciidoc/ProperDLs.yml +0 -7
  44. data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +0 -8
  45. data/assets/config-packs/vale/authoring/ButParagraph.yml +0 -8
  46. data/assets/config-packs/vale/authoring/ExNotEg.yml +0 -8
  47. data/assets/config-packs/vale/authoring/LiteralTerms.yml +0 -20
  48. data/assets/config-packs/vale/authoring/Spelling.yml +0 -679
  49. data/assets/config-packs/vale/base.ini +0 -38
  50. data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +0 -56
  51. data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +0 -121
  52. data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +0 -53
  53. data/assets/config-packs/vale/project.ini +0 -5
  54. data/assets/hooks/pre-commit +0 -63
  55. data/assets/hooks/pre-push +0 -72
  56. data/assets/scripts/adoc_section_ids.rb +0 -50
  57. data/assets/scripts/build-common.sh +0 -193
  58. data/assets/scripts/build-docker.sh +0 -64
  59. data/assets/scripts/build.sh +0 -56
  60. data/assets/scripts/parse_jekyll_asciidoc_logs.rb +0 -467
  61. data/assets/templates/Gemfile +0 -7
  62. data/assets/templates/Rakefile +0 -3
  63. data/assets/templates/gitignore +0 -69
  64. data/assets/templates/jekyll-asciidoc-fix.prompt.yml +0 -17
  65. data/assets/templates/spellcheck.prompt.yml +0 -16
  66. data/docs/agent/AGENTS.md +0 -229
  67. data/docs/agent/index.md +0 -80
  68. data/docs/agent/missions/conduct-release.md +0 -224
  69. data/docs/agent/missions/setup-new-project.md +0 -250
  70. data/docs/agent/roles/devops-release-engineer.md +0 -152
  71. data/docs/agent/roles/docops-engineer.md +0 -193
  72. data/docs/agent/roles/planner-architect.md +0 -74
  73. data/docs/agent/roles/product-engineer.md +0 -153
  74. data/docs/agent/roles/product-manager.md +0 -130
  75. data/docs/agent/roles/project-manager.md +0 -139
  76. data/docs/agent/roles/qa-testing-engineer.md +0 -115
  77. data/docs/agent/roles/tech-docs-manager.md +0 -143
  78. data/docs/agent/roles/tech-writer.md +0 -163
  79. data/docs/agent/skills/asciidoc.md +0 -609
  80. data/docs/agent/skills/code-commenting.md +0 -347
  81. data/docs/agent/skills/fix-broken-links.md +0 -309
  82. data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +0 -23
  83. data/docs/agent/skills/fix-spelling-issues.md +0 -13
  84. data/docs/agent/skills/git.md +0 -170
  85. data/docs/agent/skills/github-issues.md +0 -135
  86. data/docs/agent/skills/product-release-rollback-and-patching.md +0 -71
  87. data/docs/agent/skills/rake-cli-dev.md +0 -57
  88. data/docs/agent/skills/readme-driven-dev.md +0 -13
  89. data/docs/agent/skills/release-history.md +0 -29
  90. data/docs/agent/skills/ruby.md +0 -192
  91. data/docs/agent/skills/schemagraphy-sgyml.md +0 -18
  92. data/docs/agent/skills/tests-running.md +0 -25
  93. data/docs/agent/skills/tests-writing.md +0 -45
  94. data/docs/agent/skills/write-the-docs.md +0 -54
  95. data/docs/agent/topics/common-project-paths.md +0 -117
  96. data/docs/agent/topics/dev-tooling-usage.md +0 -202
  97. data/docs/agent/topics/devops-ci-cd.md +0 -55
  98. data/docs/agent/topics/product-docs-deployment.md +0 -25
@@ -14,7 +14,7 @@ module DocOpsLab
14
14
  FileUtils.mkdir_p('.config')
15
15
 
16
16
  # Copy template from gem
17
- FileUtils.cp(MANIFEST_DEF_PATH, MANIFEST_PATH)
17
+ FileUtils.cp(Dev.manifest_def_path, MANIFEST_PATH)
18
18
  puts "✅ Created #{MANIFEST_PATH}"
19
19
  end
20
20
 
@@ -24,7 +24,10 @@ module DocOpsLab
24
24
  return false
25
25
  end
26
26
 
27
- FileUtils.cp(GITIGNORE_STUB_SOURCE_PATH, '.gitignore')
27
+ Library.ensure_available!
28
+ stub = Library.resolve('templates/gitignore') ||
29
+ raise('Library templates/gitignore not found; run `labdev:sync:library`.')
30
+ FileUtils.cp(stub, '.gitignore')
28
31
  puts '✅ Created .gitignore file'
29
32
  true
30
33
  end
@@ -35,7 +38,10 @@ module DocOpsLab
35
38
  return false
36
39
  end
37
40
 
38
- FileUtils.cp(GEMFILE_STUB_SOURCE_PATH, 'Gemfile')
41
+ Library.ensure_available!
42
+ stub = Library.resolve('templates/Gemfile') ||
43
+ raise('Library templates/Gemfile not found; run `labdev:sync:library`.')
44
+ FileUtils.cp(stub, 'Gemfile')
39
45
  puts '✅ Created Gemfile'
40
46
  true
41
47
  end
@@ -46,7 +52,10 @@ module DocOpsLab
46
52
  return false
47
53
  end
48
54
 
49
- FileUtils.cp(RAKEFILE_STUB_SOURCE_PATH, 'Rakefile')
55
+ Library.ensure_available!
56
+ stub = Library.resolve('templates/Rakefile') ||
57
+ raise('Library templates/Rakefile not found; run `labdev:sync:library`.')
58
+ FileUtils.cp(stub, 'Rakefile')
50
59
  puts '✅ Created Rakefile'
51
60
  true
52
61
  end
@@ -62,8 +71,8 @@ module DocOpsLab
62
71
  true
63
72
  end
64
73
 
65
- def bootstrap_project
66
- puts ' Bootstrapping DocOps Lab project...'
74
+ def bootstrap_project context = nil
75
+ puts '🚀 Bootstrapping DocOps Lab project...'
67
76
  puts ''
68
77
 
69
78
  created = []
@@ -81,13 +90,27 @@ module DocOpsLab
81
90
  puts ''
82
91
  if created.any?
83
92
  puts "✅ Bootstrap complete! Created: #{created.join(', ')}"
84
- puts ''
85
- puts 'Next steps:'
86
- puts ' 1. bundle exec rake labdev:sync:all # or: docker run ... labdev:sync:all'
87
- puts ' 2. Start using labdev tasks!'
88
93
  else
89
- puts '✅ Project already initialized, nothing to create'
94
+ puts '✅ Bootstrap files already exist'
90
95
  end
96
+
97
+ # Initialize templates from manifest (if context provided, use it; else use Dev context)
98
+ context ||= Dev
99
+ puts ''
100
+ puts '🎨 Initializing templates from manifest...'
101
+ template_results = CastOps.init_cast_targets(context)
102
+
103
+ if template_results.any?
104
+ puts "✅ Initialized #{template_results.size} template(s)"
105
+ else
106
+ puts '✅ Templates already initialized'
107
+ end
108
+
109
+ puts ''
110
+ puts 'Next steps:'
111
+ puts ' 1. bundle exec rake labdev:sync:all # or: docker run ... labdev:sync:all'
112
+ puts ' 2. Review and customize the generated files'
113
+ puts ' 3. Start using labdev tasks: bundle exec rake labdev:check:env'
91
114
  end
92
115
  end
93
116
  end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'digest'
6
+ require_relative '../../dev'
7
+
8
+ module DocOpsLab
9
+ module Dev
10
+ module Library
11
+ # Manages the host-wide library cache at ~/.cache/docopslab/dev/library/.
12
+ #
13
+ # Cache layout:
14
+ # current/ Active library used for sync and resolve operations.
15
+ # previous/ Previous snapshot retained for fast rollback.
16
+ #
17
+ # The XDG_CACHE_HOME env variable is respected; defaults to ~/.cache.
18
+ module Cache
19
+ class << self
20
+ # Optional path override; set by Library.sync!/fetch! when manifest
21
+ # specifies `library.sync.cache_root`. Cleared after the operation.
22
+ attr_writer :root_override
23
+
24
+ # Absolute path to the cache root (~/.cache/docopslab/dev/library/).
25
+ # Respects +root_override+ if set, then $XDG_CACHE_HOME, then ~/.cache.
26
+ def root
27
+ return File.expand_path(@root_override) if @root_override
28
+
29
+ base = ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache'))
30
+ File.join(base, Dev.xdg_cache_subpath)
31
+ end
32
+
33
+ # Temporarily override the cache root for the duration of a block.
34
+ # Restores the previous value even if the block raises.
35
+ def with_root_override path
36
+ previous = @root_override
37
+ @root_override = path
38
+ yield
39
+ ensure
40
+ @root_override = previous
41
+ end
42
+
43
+ # Absolute path to the current library snapshot.
44
+ def current_path
45
+ File.join(root, 'current')
46
+ end
47
+
48
+ # Absolute path to the previous library snapshot (rollback target).
49
+ def previous_path
50
+ File.join(root, 'previous')
51
+ end
52
+
53
+ # Absolute path to the catalog inside the current snapshot.
54
+ def catalog_path
55
+ File.join(current_path, 'catalog.json')
56
+ end
57
+
58
+ # Load and return the parsed catalog from the current snapshot.
59
+ # Returns nil if no cache is present or the catalog is unreadable.
60
+ def catalog
61
+ return nil unless File.exist?(catalog_path)
62
+
63
+ load_catalog_json(catalog_path)
64
+ end
65
+
66
+ # True if a current snapshot with a readable catalog is present.
67
+ def available?
68
+ File.exist?(catalog_path)
69
+ end
70
+
71
+ # The remote HEAD SHA stored from the last successful fetch, or nil.
72
+ def stored_head
73
+ return nil unless File.exist?(head_path)
74
+
75
+ s = File.read(head_path).strip
76
+ s.empty? ? nil : s
77
+ rescue StandardError
78
+ nil
79
+ end
80
+
81
+ # Persist the remote HEAD SHA alongside the cache.
82
+ def write_head! sha
83
+ FileUtils.mkdir_p(root)
84
+ File.write(head_path, sha.to_s.strip)
85
+ end
86
+
87
+ # True if the cache exists and was generated within +max_age_hours+.
88
+ def fresh? max_age_hours=24
89
+ return false unless available?
90
+
91
+ ts = catalog&.dig('generated_at')
92
+ return false unless ts
93
+
94
+ (Time.now.utc - Time.parse(ts)) < max_age_hours * 3600
95
+ rescue ArgumentError
96
+ false
97
+ end
98
+
99
+ # Rotate current/ to previous/, removing any prior previous/ snapshot.
100
+ # Returns true if a rotation was performed, false if current/ was absent.
101
+ def rotate!
102
+ return false unless Dir.exist?(current_path)
103
+
104
+ FileUtils.rm_rf(previous_path)
105
+ FileUtils.mv(current_path, previous_path)
106
+ true
107
+ end
108
+
109
+ # Install a directory as the new current/ snapshot.
110
+ # Rotates any existing current/ to previous/ first.
111
+ # source_dir must be an existing directory.
112
+ def write! source_dir
113
+ raise ArgumentError, "Source directory not found: #{source_dir}" unless Dir.exist?(source_dir)
114
+
115
+ rotate! if Dir.exist?(current_path)
116
+ FileUtils.mkdir_p(File.dirname(current_path))
117
+ FileUtils.cp_r(source_dir, current_path)
118
+ true
119
+ end
120
+
121
+ # Swap previous/ back to current/.
122
+ # Returns false if no previous/ snapshot exists.
123
+ def rollback!
124
+ return false unless Dir.exist?(previous_path)
125
+
126
+ FileUtils.rm_rf(current_path)
127
+ FileUtils.mv(previous_path, current_path)
128
+ true
129
+ end
130
+
131
+ # Return a status hash describing the current snapshot.
132
+ def status
133
+ if available?
134
+ meta = catalog
135
+ {
136
+ available: true,
137
+ version: meta&.dig('library_version'),
138
+ ref: meta&.dig('library_ref'),
139
+ generated_at: meta&.dig('generated_at'),
140
+ cache_path: current_path,
141
+ has_previous: Dir.exist?(previous_path)
142
+ }
143
+ else
144
+ { available: false, cache_path: current_path }
145
+ end
146
+ end
147
+
148
+ CATALOG_JSON_KEYS = %w[library_version library_ref generated_at files].freeze
149
+
150
+ private
151
+
152
+ def head_path
153
+ File.join(root, 'remote_head')
154
+ end
155
+
156
+ # Parse catalog.json, returning the hash or nil on any error.
157
+ def load_catalog_json path
158
+ data = JSON.parse(File.read(path))
159
+ CATALOG_JSON_KEYS.all? { |k| data.key?(k) } ? data : nil
160
+ rescue JSON::ParserError
161
+ nil
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'shellwords'
5
+ require 'tmpdir'
6
+ require 'zlib'
7
+
8
+ module DocOpsLab
9
+ module Dev
10
+ module Library
11
+ # Fetches the remote asset library and installs it to the host cache.
12
+ #
13
+ # Fetch strategy (in order of preference):
14
+ # 1. `gh` CLI downloads the branch via the GitHub API tarball endpoint.
15
+ # 2. `git clone --depth=1` with sparse checkout; pulls only the library
16
+ # sub-tree from the remote branch.
17
+ #
18
+ # Auth notes:
19
+ # DocOps/lab is a public repository; no credentials are required.
20
+ # A GITHUB_TOKEN env variable can be added in a future iteration if
21
+ # private repo support is needed.
22
+ module Fetch
23
+ class << self
24
+ # Fetch the remote library and install it to the host cache.
25
+ #
26
+ # config is a hash (or nil) mirroring the `library.source` manifest
27
+ # block:
28
+ # { 'repo' => '...', 'branch' => '...', 'path' => '...' }
29
+ #
30
+ # Returns true on success, false on failure (logs a warning).
31
+ def call config={}
32
+ source = resolve_source(config)
33
+
34
+ Dir.mktmpdir('docopslab-library-') do |tmpdir|
35
+ dest = File.join(tmpdir, 'library')
36
+ FileUtils.mkdir_p(dest)
37
+
38
+ ok = fetch_git_content(source[:repo], source[:branch], source[:path], dest)
39
+ unless ok
40
+ warn '⚠️ Library fetch failed; cache not updated.'
41
+ return false
42
+ end
43
+
44
+ sha = remote_head_for_source(source)
45
+ Cache.write!(dest)
46
+ Cache.write_head!(sha) if sha
47
+ puts "✅ Library fetched and cached at #{Cache.current_path}"
48
+ true
49
+ end
50
+ rescue StandardError => e
51
+ warn "⚠️ Library fetch error: #{e.message}"
52
+ false
53
+ end
54
+
55
+ # True if the `gh` CLI is present and executable.
56
+ def gh_available?
57
+ system('gh --version > /dev/null 2>&1')
58
+ end
59
+
60
+ # True if the `git` CLI is present and executable.
61
+ def git_available?
62
+ system('git --version > /dev/null 2>&1')
63
+ end
64
+
65
+ # Look up the current HEAD SHA for the configured remote branch.
66
+ # Runs `git ls-remote` (requires git and network access.)
67
+ # Returns the SHA string, or nil on failure.
68
+ def remote_head config={}
69
+ remote_head_for_source(resolve_source(config))
70
+ end
71
+
72
+ private
73
+
74
+ def remote_head_for_source source
75
+ url = "https://github.com/#{source[:repo]}.git"
76
+ ref = "refs/heads/#{source[:branch]}"
77
+ out = `git ls-remote #{Shellwords.escape(url)} #{Shellwords.escape(ref)} 2>/dev/null`
78
+ sha = out.split("\t").first&.strip
79
+ sha&.empty? ? nil : sha
80
+ rescue StandardError
81
+ nil
82
+ end
83
+
84
+ def resolve_source config
85
+ src = config.is_a?(Hash) ? (config['source'] || config[:source] || {}) : {}
86
+ raw = src['path'] || src[:path]
87
+ {
88
+ repo: src['repo'] || src[:repo] || Dev.default_library_repo,
89
+ branch: src['branch'] || src[:branch] || Dev.default_library_branch,
90
+ path: raw&.to_s&.delete_suffix('/')
91
+ }
92
+ end
93
+
94
+ # Route to the best available CLI tool.
95
+ # Prefer git clone (simpler, transparent, no extraction layer).
96
+ # Fall back to gh tarball download if git is unavailable.
97
+ def fetch_git_content repo, branch, path, dest
98
+ if git_available?
99
+ fetch_via_git_clone(repo, branch, path, dest)
100
+ elsif gh_available?
101
+ fetch_via_gh(repo, branch, path, dest)
102
+ else
103
+ warn '⚠️ Neither `git` nor `gh` CLI is available. Cannot fetch library.'
104
+ false
105
+ end
106
+ end
107
+
108
+ # Use `gh api` to download the branch tarball, then extract the
109
+ # library sub-path.
110
+ def fetch_via_gh repo, branch, path, dest
111
+ owner, repo_name = repo.split('/', 2)
112
+ Dir.mktmpdir('docopslab-gh-') do |tmpdir|
113
+ archive = File.join(tmpdir, 'library.tar.gz')
114
+ cmd = "gh api repos/#{Shellwords.escape(owner)}/#{Shellwords.escape(repo_name)}" \
115
+ "/tarball/#{Shellwords.escape(branch)} " \
116
+ "> #{Shellwords.escape(archive)}"
117
+ unless system(cmd)
118
+ warn '⚠️ `gh api` call failed.'
119
+ return false
120
+ end
121
+ extract_subpath_from_tarball(archive, path, dest)
122
+ end
123
+ end
124
+
125
+ # Use `git clone --depth=1` to pull the library branch.
126
+ # If +path+ is given, only that subdirectory is checked out (sparse);
127
+ # otherwise the entire branch root is copied.
128
+ def fetch_via_git_clone repo, branch, path, dest
129
+ Dir.mktmpdir('docopslab-git-') do |tmpdir|
130
+ clone_dir = File.join(tmpdir, 'clone')
131
+ url = "https://github.com/#{repo}.git"
132
+ clone_flags = path ? '--filter=blob:none --sparse' : ''
133
+ clone_cmd = [
134
+ 'git clone',
135
+ '--depth=1',
136
+ "--branch #{Shellwords.escape(branch)}",
137
+ clone_flags,
138
+ Shellwords.escape(url),
139
+ Shellwords.escape(clone_dir)
140
+ ].reject(&:empty?).join(' ')
141
+
142
+ unless system(clone_cmd)
143
+ warn '⚠️ `git clone` failed.'
144
+ return false
145
+ end
146
+
147
+ if path
148
+ sparse_cmd = "git -C #{Shellwords.escape(clone_dir)} sparse-checkout set #{Shellwords.escape(path)}"
149
+ system(sparse_cmd)
150
+ end
151
+
152
+ source_path = path ? File.join(clone_dir, path) : clone_dir
153
+ unless Dir.exist?(source_path)
154
+ warn "⚠️ Library path '#{path}' not found in remote branch."
155
+ return false
156
+ end
157
+
158
+ FileUtils.cp_r("#{source_path}/.", dest)
159
+ true
160
+ end
161
+ end
162
+
163
+ # Extract all entries under `path/` from a GitHub-format tarball.
164
+ # GitHub tarballs wrap everything in a top-level `owner-repo-SHA/`
165
+ # prefix directory; this method strips that prefix automatically.
166
+ def extract_subpath_from_tarball archive, path, dest
167
+ require 'rubygems/package'
168
+
169
+ Dir.mktmpdir('docopslab-tar-') do |extract_dir|
170
+ Zlib::GzipReader.open(archive) do |gz|
171
+ Gem::Package::TarReader.new(gz) do |tar|
172
+ tar.each do |entry|
173
+ parts = entry.full_name.split('/', 2)
174
+ next if parts.length < 2
175
+
176
+ relative = parts[1]
177
+ next if relative.empty?
178
+
179
+ if path
180
+ next unless relative.start_with?("#{path}/") || relative == path
181
+
182
+ local_relative = relative.delete_prefix("#{path}/")
183
+ else
184
+ local_relative = relative
185
+ end
186
+ target = File.join(extract_dir, local_relative)
187
+
188
+ if entry.directory?
189
+ FileUtils.mkdir_p(target)
190
+ elsif entry.file?
191
+ FileUtils.mkdir_p(File.dirname(target))
192
+ File.binwrite(target, entry.read)
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ FileUtils.cp_r("#{extract_dir}/.", dest)
199
+ end
200
+ true
201
+ rescue StandardError => e
202
+ warn "⚠️ Tarball extraction failed: #{e.message}"
203
+ false
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end