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.
- checksums.yaml +4 -4
- data/README.adoc +682 -324
- data/docopslab-dev.gemspec +3 -4
- data/lib/docopslab/dev/cast_ops.rb +199 -0
- data/lib/docopslab/dev/config_manager.rb +6 -6
- data/lib/docopslab/dev/data_utils.rb +42 -0
- data/lib/docopslab/dev/docker_aware.rb +40 -0
- data/lib/docopslab/dev/file_utils.rb +18 -7
- data/lib/docopslab/dev/git_branch.rb +201 -0
- data/lib/docopslab/dev/git_hooks.rb +17 -11
- data/lib/docopslab/dev/initializer.rb +34 -11
- data/lib/docopslab/dev/library/cache.rb +167 -0
- data/lib/docopslab/dev/library/fetch.rb +209 -0
- data/lib/docopslab/dev/library.rb +341 -0
- data/lib/docopslab/dev/linters.rb +73 -15
- data/lib/docopslab/dev/manifest.rb +28 -0
- data/lib/docopslab/dev/paths.rb +0 -17
- data/lib/docopslab/dev/script_manager.rb +12 -6
- data/lib/docopslab/dev/skim.rb +109 -0
- data/lib/docopslab/dev/spell_check.rb +2 -2
- data/lib/docopslab/dev/sync_ops.rb +94 -33
- data/lib/docopslab/dev/tasks.rb +58 -18
- data/lib/docopslab/dev/version.rb +1 -1
- data/lib/docopslab/dev.rb +77 -36
- data/specs/data/default-manifest.yml +23 -5
- data/specs/data/library-index.yml +22 -0
- data/specs/data/manifest-schema.yaml +142 -4
- data/specs/data/tasks-def.yml +122 -10
- metadata +28 -73
- data/assets/config-packs/actionlint/base.yml +0 -13
- data/assets/config-packs/actionlint/project.yml +0 -13
- data/assets/config-packs/htmlproofer/base.yml +0 -27
- data/assets/config-packs/htmlproofer/project.yml +0 -25
- data/assets/config-packs/rubocop/base.yml +0 -130
- data/assets/config-packs/rubocop/project.yml +0 -8
- data/assets/config-packs/shellcheck/base.shellcheckrc +0 -14
- data/assets/config-packs/subtxt/ai-asciidoc-antipatterns.sub.txt +0 -11
- data/assets/config-packs/vale/asciidoc/ExplicitSectionIDs.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ExtraLineBeforeLevel1.yml +0 -7
- data/assets/config-packs/vale/asciidoc/OneSentencePerLine.yml +0 -8
- data/assets/config-packs/vale/asciidoc/PreferSourceBlocks.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ProperAdmonitions.yml +0 -8
- data/assets/config-packs/vale/asciidoc/ProperDLs.yml +0 -7
- data/assets/config-packs/vale/asciidoc/UncleanListStart.yml +0 -8
- data/assets/config-packs/vale/authoring/ButParagraph.yml +0 -8
- data/assets/config-packs/vale/authoring/ExNotEg.yml +0 -8
- data/assets/config-packs/vale/authoring/LiteralTerms.yml +0 -20
- data/assets/config-packs/vale/authoring/Spelling.yml +0 -679
- data/assets/config-packs/vale/base.ini +0 -38
- data/assets/config-packs/vale/config/scripts/ExplicitSectionIDs.tengo +0 -56
- data/assets/config-packs/vale/config/scripts/ExtraLineBeforeLevel1.tengo +0 -121
- data/assets/config-packs/vale/config/scripts/OneSentencePerLine.tengo +0 -53
- data/assets/config-packs/vale/project.ini +0 -5
- data/assets/hooks/pre-commit +0 -63
- data/assets/hooks/pre-push +0 -72
- data/assets/scripts/adoc_section_ids.rb +0 -50
- data/assets/scripts/build-common.sh +0 -193
- data/assets/scripts/build-docker.sh +0 -64
- data/assets/scripts/build.sh +0 -56
- data/assets/scripts/parse_jekyll_asciidoc_logs.rb +0 -467
- data/assets/templates/Gemfile +0 -7
- data/assets/templates/Rakefile +0 -3
- data/assets/templates/gitignore +0 -69
- data/assets/templates/jekyll-asciidoc-fix.prompt.yml +0 -17
- data/assets/templates/spellcheck.prompt.yml +0 -16
- data/docs/agent/AGENTS.md +0 -229
- data/docs/agent/index.md +0 -80
- data/docs/agent/missions/conduct-release.md +0 -224
- data/docs/agent/missions/setup-new-project.md +0 -250
- data/docs/agent/roles/devops-release-engineer.md +0 -152
- data/docs/agent/roles/docops-engineer.md +0 -193
- data/docs/agent/roles/planner-architect.md +0 -74
- data/docs/agent/roles/product-engineer.md +0 -153
- data/docs/agent/roles/product-manager.md +0 -130
- data/docs/agent/roles/project-manager.md +0 -139
- data/docs/agent/roles/qa-testing-engineer.md +0 -115
- data/docs/agent/roles/tech-docs-manager.md +0 -143
- data/docs/agent/roles/tech-writer.md +0 -163
- data/docs/agent/skills/asciidoc.md +0 -609
- data/docs/agent/skills/code-commenting.md +0 -347
- data/docs/agent/skills/fix-broken-links.md +0 -309
- data/docs/agent/skills/fix-jekyll-asciidoc-build-errors.md +0 -23
- data/docs/agent/skills/fix-spelling-issues.md +0 -13
- data/docs/agent/skills/git.md +0 -170
- data/docs/agent/skills/github-issues.md +0 -135
- data/docs/agent/skills/product-release-rollback-and-patching.md +0 -71
- data/docs/agent/skills/rake-cli-dev.md +0 -57
- data/docs/agent/skills/readme-driven-dev.md +0 -13
- data/docs/agent/skills/release-history.md +0 -29
- data/docs/agent/skills/ruby.md +0 -192
- data/docs/agent/skills/schemagraphy-sgyml.md +0 -18
- data/docs/agent/skills/tests-running.md +0 -25
- data/docs/agent/skills/tests-writing.md +0 -45
- data/docs/agent/skills/write-the-docs.md +0 -54
- data/docs/agent/topics/common-project-paths.md +0 -117
- data/docs/agent/topics/dev-tooling-usage.md +0 -202
- data/docs/agent/topics/devops-ci-cd.md +0 -55
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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 '✅
|
|
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
|