gemkeeper 0.4.0 → 0.5.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/.reek.yml +3 -0
- data/CHANGELOG.md +14 -1
- data/lib/gemkeeper/bundler_mirror_configurator.rb +30 -0
- data/lib/gemkeeper/cli/commands/manifest/generate.rb +63 -0
- data/lib/gemkeeper/cli/commands/manifest/validate.rb +43 -0
- data/lib/gemkeeper/cli/commands/setup.rb +5 -27
- data/lib/gemkeeper/cli/commands/sync.rb +4 -99
- data/lib/gemkeeper/cli.rb +2 -0
- data/lib/gemkeeper/gem_repo_resolver.rb +37 -30
- data/lib/gemkeeper/gem_syncer.rb +103 -0
- data/lib/gemkeeper/manifest_builder.rb +39 -0
- data/lib/gemkeeper/manifest_reader.rb +11 -14
- data/lib/gemkeeper/manifest_serializer.rb +23 -0
- data/lib/gemkeeper/manifest_validator.rb +99 -0
- data/lib/gemkeeper/rackup_process.rb +63 -0
- data/lib/gemkeeper/server_manager.rb +2 -77
- data/lib/gemkeeper/server_readiness_probe.rb +30 -0
- data/lib/gemkeeper/version.rb +1 -1
- data/lib/gemkeeper.rb +7 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f619f51cc389c49dbd52a00f22a6fc3766822a9d689eaea44d5fcfbb94e5bfe2
|
|
4
|
+
data.tar.gz: 79e3718a784f510d72156f071551af20bba3c2d9177ed4b9a2e9791917337947
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03ce5eeb1e672cc09815d80a505a0cf2974bbd378c0f3c38245168100a82b59783a45aacd2afb149b0e9814df8ecbd7114fb1eda4738dc8bfa772c5cfa104809
|
|
7
|
+
data.tar.gz: f26bf3d36e183c2aa7585a36349599092f84347fb45f6325b21c196e37b8893a6102afc7a169efa50a880c408c406ca09a0b80af7e5bd87dbac0b5538960e509
|
data/.reek.yml
CHANGED
|
@@ -36,3 +36,6 @@ detectors:
|
|
|
36
36
|
- Gemkeeper::CLI::Commands::Server::Start
|
|
37
37
|
- Gemkeeper::CLI::Commands::Server::Stop
|
|
38
38
|
- Gemkeeper::CLI::Commands::Server::Status
|
|
39
|
+
- Gemkeeper::CLI::Commands::Manifest
|
|
40
|
+
- Gemkeeper::CLI::Commands::Manifest::Generate
|
|
41
|
+
- Gemkeeper::CLI::Commands::Manifest::Validate
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-05-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `gemkeeper manifest generate LOCKFILE_PATH` builds or updates the gem manifest from a Gemfile.lock.
|
|
8
|
+
Merges with any existing manifest by default; `--force` overwrites it entirely.
|
|
9
|
+
Accepts `--manifest` to specify a non-default path.
|
|
10
|
+
- `gemkeeper manifest validate [PATH]` checks a manifest file for structural errors: missing fields, invalid repo URLs, duplicate names, and malformed `source_url`.
|
|
11
|
+
`--resolve` additionally probes each repo with `git ls-remote` (5s timeout per entry) to verify reachability.
|
|
12
|
+
- During interactive `gemkeeper setup`, gems with inaccessible source repos can now be skipped.
|
|
13
|
+
Enter nothing when no URL can be inferred, or type `skip` at any prompt.
|
|
14
|
+
|
|
3
15
|
## [0.4.0] - 2026-05-27
|
|
4
16
|
|
|
5
17
|
### Added
|
|
@@ -55,7 +67,8 @@
|
|
|
55
67
|
|
|
56
68
|
- Initial release
|
|
57
69
|
|
|
58
|
-
[Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.
|
|
70
|
+
[Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.5.0...HEAD
|
|
71
|
+
[0.5.0]: https://github.com/danhorst/gemkeeper/compare/0.4.0...0.5.0
|
|
59
72
|
[0.4.0]: https://github.com/danhorst/gemkeeper/compare/0.3.0...0.4.0
|
|
60
73
|
[0.3.0]: https://github.com/danhorst/gemkeeper/compare/0.2.1...0.3.0
|
|
61
74
|
[0.2.1]: https://github.com/danhorst/gemkeeper/compare/0.2.0...0.2.1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
# Configures Bundler mirror settings so private gem registries proxy through local Geminabox.
|
|
5
|
+
class BundlerMirrorConfigurator
|
|
6
|
+
def initialize(candidates, port:, global:)
|
|
7
|
+
@remotes = candidates.filter_map { |c| c[:remote] if c[:source_type] == :private_gem }.uniq
|
|
8
|
+
@local_url = "http://localhost:#{port}"
|
|
9
|
+
@scope = global ? "--global" : "--local"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def configure(output: $stdout)
|
|
13
|
+
return if @remotes.empty?
|
|
14
|
+
|
|
15
|
+
output.puts ""
|
|
16
|
+
@remotes.each { |remote| configure_mirror(remote, output) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def configure_mirror(remote, output)
|
|
22
|
+
if system("bundle", "config", "set", @scope, "mirror.#{remote}", @local_url, out: File::NULL)
|
|
23
|
+
output.puts "Configured: bundle config set #{@scope} mirror.#{remote} #{@local_url}"
|
|
24
|
+
else
|
|
25
|
+
warn "Warning: failed to configure bundler mirror for #{remote}"
|
|
26
|
+
warn " Run manually: bundle config set #{@scope} mirror.#{remote} #{@local_url}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Manifest
|
|
7
|
+
class Generate < Dry::CLI::Command
|
|
8
|
+
desc "Build or update the gem manifest from a Gemfile.lock"
|
|
9
|
+
|
|
10
|
+
argument :lockfile_path, type: :string, required: true,
|
|
11
|
+
desc: "Path to the project's Gemfile.lock"
|
|
12
|
+
option :manifest, type: :string,
|
|
13
|
+
desc: "Path to write manifest (default: ~/.config/gemkeeper/manifest.yml)"
|
|
14
|
+
option :force, type: :boolean, default: false,
|
|
15
|
+
desc: "Overwrite existing manifest entirely"
|
|
16
|
+
|
|
17
|
+
def call(lockfile_path:, **options)
|
|
18
|
+
path = manifest_path(options)
|
|
19
|
+
manifest = ManifestReader.load(path)
|
|
20
|
+
manifest.clear! if options[:force]
|
|
21
|
+
result = ManifestBuilder.build(lockfile_path:, manifest:)
|
|
22
|
+
|
|
23
|
+
if result.empty?
|
|
24
|
+
puts "No internal gem sources found in #{lockfile_path}"
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if result.any_changes?
|
|
29
|
+
manifest.save(path)
|
|
30
|
+
puts "Wrote #{path}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
print_summary(result, result.any_changes? ? nil : "Manifest up to date, no changes")
|
|
34
|
+
rescue UnresolvableGemError, ManifestConflictError => error
|
|
35
|
+
warn "Error: #{error.message}"
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def manifest_path(options)
|
|
42
|
+
options[:manifest] || ManifestReader::DEFAULT_PATH
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def print_summary(result, no_change_message)
|
|
46
|
+
if no_change_message
|
|
47
|
+
puts no_change_message
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
parts = []
|
|
52
|
+
parts << "#{result.added_count} added" if result.added_count.positive?
|
|
53
|
+
parts << "#{result.skipped_count} skipped" if result.skipped_count.positive?
|
|
54
|
+
parts << "#{result.already_mapped_count} already mapped" if result.already_mapped_count.positive?
|
|
55
|
+
puts parts.join(", ") if parts.any?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
register "manifest generate", Commands::Manifest::Generate
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Manifest
|
|
7
|
+
class Validate < Dry::CLI::Command
|
|
8
|
+
desc "Validate a gem manifest file"
|
|
9
|
+
|
|
10
|
+
argument :path, type: :string, required: false,
|
|
11
|
+
desc: "Path to manifest (default: ~/.config/gemkeeper/manifest.yml)"
|
|
12
|
+
option :resolve, type: :boolean, default: false,
|
|
13
|
+
desc: "Also verify each repo is reachable via git ls-remote"
|
|
14
|
+
|
|
15
|
+
def call(path: nil, **options)
|
|
16
|
+
manifest_path = path || ManifestReader::DEFAULT_PATH
|
|
17
|
+
validator = ManifestValidator.new(manifest_path)
|
|
18
|
+
|
|
19
|
+
puts "Checking #{manifest_path}..." if options[:resolve]
|
|
20
|
+
errors = validator.validate(resolve: options[:resolve], output: $stdout)
|
|
21
|
+
|
|
22
|
+
if errors.empty?
|
|
23
|
+
entry_count = entry_count(manifest_path)
|
|
24
|
+
puts "#{manifest_path}: valid (#{entry_count} #{entry_count == 1 ? "entry" : "entries"})"
|
|
25
|
+
else
|
|
26
|
+
warn "#{manifest_path}: #{errors.size} #{"error".then { |w| errors.size == 1 ? w : "#{w}s" }}"
|
|
27
|
+
errors.each { |e| warn " #{e}" }
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def entry_count(path)
|
|
35
|
+
ManifestReader.load(path).gems.size
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
register "manifest validate", Commands::Manifest::Validate
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -40,12 +40,8 @@ module Gemkeeper
|
|
|
40
40
|
|
|
41
41
|
def setup_from_lockfile(lockfile_path, output_path, options)
|
|
42
42
|
manifest = load_manifest(options)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
unless candidates.empty?
|
|
46
|
-
GemRepoResolver.new(candidates:, manifest:).resolve!
|
|
47
|
-
manifest.save(manifest_path(options))
|
|
48
|
-
end
|
|
43
|
+
result = ManifestBuilder.build(lockfile_path:, manifest:)
|
|
44
|
+
manifest.save(manifest_path(options)) if result.any_changes?
|
|
49
45
|
|
|
50
46
|
lockfile_versions = LockfileParser.parse(lockfile_path)
|
|
51
47
|
global_output_path = options[:global] ? output_path : nil
|
|
@@ -54,26 +50,10 @@ module Gemkeeper
|
|
|
54
50
|
|
|
55
51
|
File.write(output_path, config.to_yaml)
|
|
56
52
|
puts "Wrote #{output_path}"
|
|
57
|
-
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def configure_bundler(candidates, config, options)
|
|
61
|
-
remotes = candidates.filter_map { |c| c[:remote] if c[:source_type] == :private_gem }.uniq
|
|
62
|
-
return if remotes.empty?
|
|
53
|
+
return if options[:skip_bundler_config]
|
|
63
54
|
|
|
64
55
|
port = config.fetch("port", Configuration::DEFAULT_PORT)
|
|
65
|
-
|
|
66
|
-
scope = options[:global] ? "--global" : "--local"
|
|
67
|
-
|
|
68
|
-
puts ""
|
|
69
|
-
remotes.each do |remote|
|
|
70
|
-
if system("bundle", "config", "set", scope, "mirror.#{remote}", local_url, out: File::NULL)
|
|
71
|
-
puts "Configured: bundle config set #{scope} mirror.#{remote} #{local_url}"
|
|
72
|
-
else
|
|
73
|
-
warn "Warning: failed to configure bundler mirror for #{remote}"
|
|
74
|
-
warn " Run manually: bundle config set #{scope} mirror.#{remote} #{local_url}"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
56
|
+
BundlerMirrorConfigurator.new(result.candidates, port:, global: options[:global]).configure
|
|
77
57
|
end
|
|
78
58
|
|
|
79
59
|
def setup_from_config(source_path, output_path, options)
|
|
@@ -105,9 +85,7 @@ module Gemkeeper
|
|
|
105
85
|
existing_gems = existing["gems"] || []
|
|
106
86
|
source_gems = source["gems"] || []
|
|
107
87
|
existing_repos = existing_gems.to_set { |g| g["repo"] }
|
|
108
|
-
|
|
109
|
-
source_gems.each { |g| merged << g unless existing_repos.include?(g["repo"]) }
|
|
110
|
-
merged
|
|
88
|
+
existing_gems + source_gems.reject { |g| existing_repos.include?(g["repo"]) }
|
|
111
89
|
end
|
|
112
90
|
|
|
113
91
|
def load_manifest(options)
|
|
@@ -12,8 +12,8 @@ module Gemkeeper
|
|
|
12
12
|
def call(gem_name: nil, **options)
|
|
13
13
|
config = Configuration.load(options[:config])
|
|
14
14
|
gems_to_sync = select_gems(config, gem_name)
|
|
15
|
-
|
|
16
|
-
counts, failures = run_sync(gems_to_sync,
|
|
15
|
+
syncer = GemSyncer.new(config, GemUploader.new(config.geminabox_url))
|
|
16
|
+
counts, failures = run_sync(gems_to_sync, syncer)
|
|
17
17
|
report_results(counts, failures, gems_to_sync.size)
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -36,12 +36,11 @@ module Gemkeeper
|
|
|
36
36
|
gems
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def run_sync(gems_to_sync,
|
|
39
|
+
def run_sync(gems_to_sync, syncer)
|
|
40
40
|
counts = { synced: 0, skipped: 0 }
|
|
41
41
|
failures = []
|
|
42
42
|
gems_to_sync.each do |gem_def|
|
|
43
|
-
|
|
44
|
-
counts[result] += 1
|
|
43
|
+
counts[syncer.sync(gem_def)] += 1
|
|
45
44
|
rescue Error => error
|
|
46
45
|
failures << { name: gem_def.name, message: error.message }
|
|
47
46
|
end
|
|
@@ -64,100 +63,6 @@ module Gemkeeper
|
|
|
64
63
|
failures.each { |f| warn " #{f[:name]}: #{f[:message]}" }
|
|
65
64
|
exit 1
|
|
66
65
|
end
|
|
67
|
-
|
|
68
|
-
def sync_gem(gem_def, config, uploader)
|
|
69
|
-
name = gem_def.name
|
|
70
|
-
repo_url = gem_def.repo
|
|
71
|
-
gems_path = config.gems_path
|
|
72
|
-
version = resolve_version(gem_def)
|
|
73
|
-
|
|
74
|
-
latest = gem_def.latest?
|
|
75
|
-
return :skipped if !latest && cached?(name, version, gems_path)
|
|
76
|
-
|
|
77
|
-
puts "Syncing #{name} @ #{version}..."
|
|
78
|
-
|
|
79
|
-
local_path = File.join(config.repos_path, name)
|
|
80
|
-
repo = GitRepository.new(repo_url, local_path)
|
|
81
|
-
|
|
82
|
-
Output.step("Fetching from #{repo_url}...")
|
|
83
|
-
fetch_repo(repo, repo_url)
|
|
84
|
-
|
|
85
|
-
checkout_gem_version(repo, version)
|
|
86
|
-
|
|
87
|
-
if latest
|
|
88
|
-
version = repo.current_version or
|
|
89
|
-
raise BuildError, "Could not read version from gemspec in #{repo_url}"
|
|
90
|
-
return :skipped if cached?(name, version, gems_path)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
Output.step("Building gem...")
|
|
94
|
-
gem_path = GemBuilder.new(local_path, gems_path).build
|
|
95
|
-
|
|
96
|
-
Output.step("Uploading to Geminabox...")
|
|
97
|
-
result = uploader.upload(gem_path)
|
|
98
|
-
Output.step(result[:message])
|
|
99
|
-
Output.success(" Done!")
|
|
100
|
-
:synced
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def resolve_version(gem_def)
|
|
104
|
-
return gem_def.version unless gem_def.from_lockfile?
|
|
105
|
-
|
|
106
|
-
name = gem_def.name
|
|
107
|
-
lockfile_path = LockfileParser.find
|
|
108
|
-
unless lockfile_path
|
|
109
|
-
raise GitError,
|
|
110
|
-
"version: from_lockfile for #{name} — no Gemfile.lock found in " \
|
|
111
|
-
"#{Dir.pwd} or any parent directory"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
versions = LockfileParser.parse(lockfile_path)
|
|
115
|
-
version = versions[name]
|
|
116
|
-
raise GitError, "#{name} not found in #{lockfile_path}" unless version
|
|
117
|
-
|
|
118
|
-
version
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def cached?(name, version, gems_path)
|
|
122
|
-
bare_version = version.delete_prefix("v")
|
|
123
|
-
gem_file = File.join(gems_path, "gems", "#{name}-#{bare_version}.gem")
|
|
124
|
-
if File.exist?(gem_file)
|
|
125
|
-
Output.skip("Skipping #{name} @ #{bare_version} (already cached)")
|
|
126
|
-
true
|
|
127
|
-
else
|
|
128
|
-
false
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def fetch_repo(repo, repo_url)
|
|
133
|
-
repo.clone_or_pull
|
|
134
|
-
rescue GitError => git_error
|
|
135
|
-
raise auth_error?(git_error) ? auth_failure_error(repo_url, git_error) : git_error
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def checkout_gem_version(repo, version)
|
|
139
|
-
Output.step("Checking out #{version}...")
|
|
140
|
-
repo.checkout_version(version)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def auth_error?(error)
|
|
144
|
-
auth_patterns = [
|
|
145
|
-
/authentication failed/i,
|
|
146
|
-
/could not read from remote repository/i,
|
|
147
|
-
/permission denied \(publickey\)/i,
|
|
148
|
-
/repository not found/i,
|
|
149
|
-
/fatal: credential/i
|
|
150
|
-
]
|
|
151
|
-
auth_patterns.any? { |pat| error.message.match?(pat) }
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def auth_failure_error(repo_url, original_error)
|
|
155
|
-
GitError.new(
|
|
156
|
-
"Git authentication failed for #{repo_url}.\n" \
|
|
157
|
-
"#{original_error.message}\n" \
|
|
158
|
-
"Configure GitHub credentials: https://docs.github.com/en/authentication"
|
|
159
|
-
)
|
|
160
|
-
end
|
|
161
66
|
end
|
|
162
67
|
end
|
|
163
68
|
|
data/lib/gemkeeper/cli.rb
CHANGED
|
@@ -19,3 +19,5 @@ require_relative "cli/commands/list"
|
|
|
19
19
|
require_relative "cli/commands/server/start"
|
|
20
20
|
require_relative "cli/commands/server/stop"
|
|
21
21
|
require_relative "cli/commands/server/status"
|
|
22
|
+
require_relative "cli/commands/manifest/generate"
|
|
23
|
+
require_relative "cli/commands/manifest/validate"
|
|
@@ -5,6 +5,8 @@ module Gemkeeper
|
|
|
5
5
|
# GIT-sourced gems are added automatically; private gem registry entries
|
|
6
6
|
# are inferred where possible (GitHub Packages) or prompted interactively.
|
|
7
7
|
class GemRepoResolver
|
|
8
|
+
SKIP_INPUT = "skip"
|
|
9
|
+
|
|
8
10
|
def initialize(candidates:, manifest:, input: $stdin, output: $stdout)
|
|
9
11
|
@candidates = candidates
|
|
10
12
|
@manifest = manifest
|
|
@@ -14,37 +16,36 @@ module Gemkeeper
|
|
|
14
16
|
|
|
15
17
|
def resolve!
|
|
16
18
|
unresolvable = []
|
|
17
|
-
|
|
18
|
-
@candidates.each do |candidate|
|
|
19
|
-
name = candidate[:name]
|
|
20
|
-
next if @manifest.repo_for(name)
|
|
21
|
-
|
|
22
|
-
if candidate[:source_type] == :git
|
|
23
|
-
@manifest.add_mapping(name:, repo: candidate[:repo])
|
|
24
|
-
else
|
|
25
|
-
repo = resolve_private_gem(candidate)
|
|
26
|
-
if repo
|
|
27
|
-
@manifest.add_mapping(name:, repo:)
|
|
28
|
-
else
|
|
29
|
-
unresolvable << candidate
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
19
|
+
@candidates.each { |candidate| resolve_candidate(candidate, unresolvable) }
|
|
34
20
|
raise_unresolvable(unresolvable) if unresolvable.any?
|
|
35
21
|
@manifest
|
|
36
22
|
end
|
|
37
23
|
|
|
38
24
|
private
|
|
39
25
|
|
|
40
|
-
def
|
|
41
|
-
|
|
26
|
+
def resolve_candidate(candidate, unresolvable)
|
|
27
|
+
return if @manifest.repo_for(candidate[:name])
|
|
28
|
+
return @manifest.add_mapping(name: candidate[:name], repo: candidate[:repo]) if candidate[:source_type] == :git
|
|
29
|
+
|
|
30
|
+
interactive? ? resolve_interactively(candidate) : resolve_non_interactively(candidate, unresolvable)
|
|
31
|
+
end
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
def resolve_interactively(candidate)
|
|
34
|
+
repo = prompt(candidate, infer_repo(candidate))
|
|
35
|
+
if repo
|
|
36
|
+
@manifest.add_mapping(name: candidate[:name], repo:)
|
|
37
|
+
else
|
|
38
|
+
@output.puts " Skipping #{candidate[:name]}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resolve_non_interactively(candidate, unresolvable)
|
|
43
|
+
repo = infer_repo(candidate)
|
|
44
|
+
if repo
|
|
45
|
+
warn "Note: auto-inferred repo for #{candidate[:name]}: #{repo}"
|
|
46
|
+
@manifest.add_mapping(name: candidate[:name], repo:)
|
|
47
|
+
else
|
|
48
|
+
unresolvable << candidate
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
|
|
@@ -57,12 +58,18 @@ module Gemkeeper
|
|
|
57
58
|
|
|
58
59
|
def prompt(candidate, inferred)
|
|
59
60
|
@output.print "\n #{candidate[:name]} (from #{candidate[:remote]})"
|
|
60
|
-
@output.print "\n Repo URL"
|
|
61
|
-
@
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
@output.print "\n Repo URL#{prompt_hint(inferred)}: "
|
|
62
|
+
parse_prompt_input(@input.gets&.strip, inferred)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def prompt_hint(inferred)
|
|
66
|
+
inferred ? " [#{inferred}] (or \"#{SKIP_INPUT}\" to skip)" : " (blank to skip)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_prompt_input(input, inferred)
|
|
70
|
+
return nil if input.nil? || input == SKIP_INPUT || (input.empty? && inferred.nil?)
|
|
71
|
+
|
|
72
|
+
input.empty? ? inferred : input
|
|
66
73
|
end
|
|
67
74
|
|
|
68
75
|
def interactive?
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
# Syncs a single gem: resolves version, checks cache, clones/pulls, builds, uploads.
|
|
5
|
+
class GemSyncer
|
|
6
|
+
AUTH_ERROR_PATTERNS = [
|
|
7
|
+
/authentication failed/i,
|
|
8
|
+
/could not read from remote repository/i,
|
|
9
|
+
/permission denied \(publickey\)/i,
|
|
10
|
+
/repository not found/i,
|
|
11
|
+
/fatal: credential/i
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(config, uploader)
|
|
15
|
+
@config = config
|
|
16
|
+
@uploader = uploader
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sync(gem_def)
|
|
20
|
+
version = resolve_version(gem_def)
|
|
21
|
+
name = gem_def.name
|
|
22
|
+
gems_path = @config.gems_path
|
|
23
|
+
|
|
24
|
+
return :skipped if !gem_def.latest? && cached?(name, version, gems_path)
|
|
25
|
+
|
|
26
|
+
puts "Syncing #{name} @ #{version}..."
|
|
27
|
+
local_path = File.join(@config.repos_path, name)
|
|
28
|
+
repo = fetch_repo(gem_def.repo, local_path)
|
|
29
|
+
|
|
30
|
+
Output.step("Checking out #{version}...")
|
|
31
|
+
repo.checkout_version(version)
|
|
32
|
+
|
|
33
|
+
if gem_def.latest?
|
|
34
|
+
version = latest_version!(repo, name, gems_path, gem_def.repo)
|
|
35
|
+
return :skipped unless version
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
build_and_upload(local_path, gems_path)
|
|
39
|
+
:synced
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def resolve_version(gem_def)
|
|
45
|
+
return gem_def.version unless gem_def.from_lockfile?
|
|
46
|
+
|
|
47
|
+
lockfile_path = LockfileParser.find
|
|
48
|
+
unless lockfile_path
|
|
49
|
+
raise GitError,
|
|
50
|
+
"version: from_lockfile for #{gem_def.name} — no Gemfile.lock found in " \
|
|
51
|
+
"#{Dir.pwd} or any parent directory"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
versions = LockfileParser.parse(lockfile_path)
|
|
55
|
+
versions[gem_def.name] or raise GitError, "#{gem_def.name} not found in #{lockfile_path}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cached?(name, version, gems_path)
|
|
59
|
+
bare = version.delete_prefix("v")
|
|
60
|
+
gem_file = File.join(gems_path, "gems", "#{name}-#{bare}.gem")
|
|
61
|
+
return false unless File.exist?(gem_file)
|
|
62
|
+
|
|
63
|
+
Output.skip("Skipping #{name} @ #{bare} (already cached)")
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fetch_repo(repo_url, local_path)
|
|
68
|
+
repo = GitRepository.new(repo_url, local_path)
|
|
69
|
+
Output.step("Fetching from #{repo_url}...")
|
|
70
|
+
repo.clone_or_pull
|
|
71
|
+
repo
|
|
72
|
+
rescue GitError => git_error
|
|
73
|
+
raise auth_error?(git_error) ? auth_failure_error(repo_url, git_error) : git_error
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def latest_version!(repo, name, gems_path, repo_url)
|
|
77
|
+
version = repo.current_version or
|
|
78
|
+
raise BuildError, "Could not read version from gemspec in #{repo_url}"
|
|
79
|
+
cached?(name, version, gems_path) ? nil : version
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_and_upload(local_path, gems_path)
|
|
83
|
+
Output.step("Building gem...")
|
|
84
|
+
gem_path = GemBuilder.new(local_path, gems_path).build
|
|
85
|
+
Output.step("Uploading to Geminabox...")
|
|
86
|
+
result = @uploader.upload(gem_path)
|
|
87
|
+
Output.step(result[:message])
|
|
88
|
+
Output.success(" Done!")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def auth_error?(error)
|
|
92
|
+
AUTH_ERROR_PATTERNS.any? { |pat| error.message.match?(pat) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def auth_failure_error(repo_url, original_error)
|
|
96
|
+
GitError.new(
|
|
97
|
+
"Git authentication failed for #{repo_url}.\n" \
|
|
98
|
+
"#{original_error.message}\n" \
|
|
99
|
+
"Configure GitHub credentials: https://docs.github.com/en/authentication"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
# Builds or updates a manifest from a Gemfile.lock, tracking what changed.
|
|
5
|
+
class ManifestBuilder
|
|
6
|
+
Result = Struct.new(:manifest, :candidates, :added_count, :skipped_count, :already_mapped_count,
|
|
7
|
+
keyword_init: true) do
|
|
8
|
+
def any_changes?
|
|
9
|
+
added_count.positive?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def empty?
|
|
13
|
+
candidates.empty?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.build(lockfile_path:, manifest:, input: $stdin, output: $stdout)
|
|
18
|
+
new(lockfile_path:, manifest:).build!(input:, output:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(lockfile_path:, manifest:)
|
|
22
|
+
@lockfile_path = lockfile_path
|
|
23
|
+
@manifest = manifest
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build!(input: $stdin, output: $stdout)
|
|
27
|
+
candidates = LockfileParser.internal_sources(@lockfile_path)
|
|
28
|
+
already_mapped_count = candidates.count { |c| @manifest.repo_for(c[:name]) }
|
|
29
|
+
before_size = @manifest.gems.size
|
|
30
|
+
|
|
31
|
+
GemRepoResolver.new(candidates:, manifest: @manifest, input:, output:).resolve! unless candidates.empty?
|
|
32
|
+
|
|
33
|
+
added_count = @manifest.gems.size - before_size
|
|
34
|
+
skipped_count = candidates.size - already_mapped_count - added_count
|
|
35
|
+
|
|
36
|
+
Result.new(manifest: @manifest, candidates:, added_count:, skipped_count:, already_mapped_count:)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
|
|
6
3
|
module Gemkeeper
|
|
7
4
|
# Decouples manifest format from sync/setup commands that need the eligible gem list.
|
|
8
5
|
class ManifestReader
|
|
@@ -18,7 +15,13 @@ module Gemkeeper
|
|
|
18
15
|
@path = path
|
|
19
16
|
@gems = []
|
|
20
17
|
@source_url = nil
|
|
21
|
-
|
|
18
|
+
load_from_disk if File.exist?(@path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear!
|
|
22
|
+
@gems = []
|
|
23
|
+
@source_url = nil
|
|
24
|
+
self
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def gem_names
|
|
@@ -46,21 +49,15 @@ module Gemkeeper
|
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def save(path = @path)
|
|
49
|
-
|
|
50
|
-
data = {}
|
|
51
|
-
data["source_url"] = @source_url if @source_url
|
|
52
|
-
data["gems"] = @gems.map { |g| { "name" => g[:name], "repo" => g[:repo] } }
|
|
53
|
-
File.write(path, data.to_yaml)
|
|
52
|
+
ManifestSerializer.save(path, gems: @gems, source_url: @source_url)
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
private
|
|
57
56
|
|
|
58
|
-
def
|
|
59
|
-
data =
|
|
57
|
+
def load_from_disk
|
|
58
|
+
data = ManifestSerializer.load(@path)
|
|
60
59
|
@source_url = data[:source_url]&.to_s
|
|
61
|
-
@gems = (data[:gems] || []).map
|
|
62
|
-
{ name: entry[:name].to_s, repo: entry[:repo].to_s }
|
|
63
|
-
end
|
|
60
|
+
@gems = (data[:gems] || []).map { |entry| { name: entry[:name].to_s, repo: entry[:repo].to_s } }
|
|
64
61
|
end
|
|
65
62
|
|
|
66
63
|
def conflict_message(name, existing_repo, new_repo)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Gemkeeper
|
|
7
|
+
# Handles YAML read/write for the gem manifest file.
|
|
8
|
+
module ManifestSerializer
|
|
9
|
+
def self.load(path)
|
|
10
|
+
return {} unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
YAML.safe_load_file(path, permitted_classes: [], symbolize_names: true) || {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.save(path, gems:, source_url:)
|
|
16
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
17
|
+
data = {}
|
|
18
|
+
data["source_url"] = source_url if source_url
|
|
19
|
+
data["gems"] = gems.map { |g| { "name" => g[:name], "repo" => g[:repo] } }
|
|
20
|
+
File.write(path, data.to_yaml)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Gemkeeper
|
|
8
|
+
# Validates a manifest file structurally and, optionally, by probing git remotes.
|
|
9
|
+
class ManifestValidator
|
|
10
|
+
VALID_GIT_URL = %r{\A(git@|https?://|ssh://)}
|
|
11
|
+
VALID_HTTP_URL = %r{\Ahttps?://}
|
|
12
|
+
RESOLVE_TIMEOUT = 5
|
|
13
|
+
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate(resolve: false, output: $stdout)
|
|
19
|
+
errors = static_errors
|
|
20
|
+
return errors if errors.any?
|
|
21
|
+
|
|
22
|
+
errors + (resolve ? resolve_errors(output) : [])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def static_errors
|
|
28
|
+
return ["File not found: #{@path}"] unless File.exist?(@path)
|
|
29
|
+
|
|
30
|
+
data = load_yaml
|
|
31
|
+
return [data] if data.is_a?(String)
|
|
32
|
+
|
|
33
|
+
structure_errors(data) + entry_errors(data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_yaml
|
|
37
|
+
@data = YAML.safe_load_file(@path, permitted_classes: [], symbolize_names: false) || {}
|
|
38
|
+
rescue Psych::SyntaxError => e
|
|
39
|
+
"Invalid YAML: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def structure_errors(data)
|
|
43
|
+
errors = []
|
|
44
|
+
source_url = data["source_url"].to_s
|
|
45
|
+
errors << "source_url is not a valid HTTP(S) URL" if data.key?("source_url") && !source_url.match?(VALID_HTTP_URL)
|
|
46
|
+
errors << "gems must be a list" if data.key?("gems") && !data["gems"].is_a?(Array)
|
|
47
|
+
errors
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def entry_errors(data)
|
|
51
|
+
return [] unless data["gems"].is_a?(Array)
|
|
52
|
+
|
|
53
|
+
seen = Set.new
|
|
54
|
+
data["gems"].each_with_index.flat_map { |entry, i| single_entry_errors(entry, i, seen) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def single_entry_errors(entry, index, seen)
|
|
58
|
+
name = entry["name"].to_s.strip
|
|
59
|
+
repo = entry["repo"].to_s.strip
|
|
60
|
+
label = name.empty? ? "gems[#{index}]" : "gems[#{index}] (#{name})"
|
|
61
|
+
|
|
62
|
+
field_errors(label, name, repo) + duplicate_errors(label, name, seen)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def field_errors(label, name, repo)
|
|
66
|
+
errors = []
|
|
67
|
+
errors << "#{label}: missing name" if name.empty?
|
|
68
|
+
errors << "#{label}: missing repo" if repo.empty?
|
|
69
|
+
errors << "#{label}: repo does not look like a git URL" if !repo.empty? && !repo.match?(VALID_GIT_URL)
|
|
70
|
+
errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def duplicate_errors(label, name, seen)
|
|
74
|
+
return [] if name.empty?
|
|
75
|
+
|
|
76
|
+
if seen.include?(name)
|
|
77
|
+
["#{label}: duplicate name"]
|
|
78
|
+
else
|
|
79
|
+
seen << name
|
|
80
|
+
[]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_errors(output)
|
|
85
|
+
(@data["gems"] || []).flat_map { |entry| probe_repo(entry["name"], entry["repo"], output) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def probe_repo(name, repo, output)
|
|
89
|
+
reachable = Timeout.timeout(RESOLVE_TIMEOUT) do
|
|
90
|
+
system("git", "ls-remote", repo, "--quiet", out: File::NULL, err: File::NULL)
|
|
91
|
+
end
|
|
92
|
+
output.puts " #{name}: #{reachable ? "reachable" : "FAILED"}"
|
|
93
|
+
reachable ? [] : ["#{name} (#{repo}): unreachable"]
|
|
94
|
+
rescue Timeout::Error
|
|
95
|
+
output.puts " #{name}: timed out"
|
|
96
|
+
["#{name} (#{repo}): timed out after #{RESOLVE_TIMEOUT}s"]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Gemkeeper
|
|
8
|
+
# Builds config.ru, spawns the rackup/puma process, and waits for HTTP readiness.
|
|
9
|
+
class RackupProcess
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
generate_config_ru
|
|
16
|
+
Dir.chdir(@config.cache_dir) { launch_daemon }
|
|
17
|
+
wait_for_server
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start_foreground
|
|
21
|
+
generate_config_ru
|
|
22
|
+
Dir.chdir(@config.cache_dir) { system(*build_rackup_cmd) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def generate_config_ru
|
|
28
|
+
FileUtils.mkdir_p(@config.cache_dir)
|
|
29
|
+
FileUtils.mkdir_p(@config.gems_path)
|
|
30
|
+
File.write(@config.config_ru_path, config_ru_content)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def config_ru_content
|
|
34
|
+
gems_path = @config.gems_path
|
|
35
|
+
<<~RUBY
|
|
36
|
+
# frozen_string_literal: true
|
|
37
|
+
# Auto-generated by Gemkeeper
|
|
38
|
+
|
|
39
|
+
require "rubygems/indexer"
|
|
40
|
+
require "geminabox"
|
|
41
|
+
|
|
42
|
+
Geminabox.data = #{gems_path.inspect}
|
|
43
|
+
Geminabox.rubygems_proxy = true
|
|
44
|
+
|
|
45
|
+
run Geminabox::Server
|
|
46
|
+
RUBY
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_rackup_cmd(*extra)
|
|
50
|
+
["rackup", @config.config_ru_path, "--host", "127.0.0.1",
|
|
51
|
+
"-p", @config.port.to_s, "-s", "puma", *extra]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def launch_daemon
|
|
55
|
+
_stdout, stderr, status = Open3.capture3(*build_rackup_cmd("-D", "-P", @config.pid_file))
|
|
56
|
+
raise ServerError, "Failed to start server:\n#{stderr}" unless status.success? || File.exist?(@config.pid_file)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def wait_for_server
|
|
60
|
+
ServerReadinessProbe.new(@config.geminabox_url).wait
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "open3"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
|
|
6
5
|
module Gemkeeper
|
|
@@ -14,14 +13,12 @@ module Gemkeeper
|
|
|
14
13
|
|
|
15
14
|
def start
|
|
16
15
|
ensure_not_running!
|
|
17
|
-
|
|
18
|
-
start_server
|
|
16
|
+
RackupProcess.new(config).start
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def start_foreground
|
|
22
20
|
ensure_not_running!
|
|
23
|
-
|
|
24
|
-
start_server_foreground
|
|
21
|
+
RackupProcess.new(config).start_foreground
|
|
25
22
|
end
|
|
26
23
|
|
|
27
24
|
def stop
|
|
@@ -60,58 +57,6 @@ module Gemkeeper
|
|
|
60
57
|
raise ServerAlreadyRunningError, "Server is already running (PID: #{read_pid})" if running?
|
|
61
58
|
end
|
|
62
59
|
|
|
63
|
-
def generate_config_ru
|
|
64
|
-
gems_path = config.gems_path
|
|
65
|
-
FileUtils.mkdir_p(config.cache_dir)
|
|
66
|
-
FileUtils.mkdir_p(gems_path)
|
|
67
|
-
|
|
68
|
-
content = <<~RUBY
|
|
69
|
-
# frozen_string_literal: true
|
|
70
|
-
# Auto-generated by Gemkeeper
|
|
71
|
-
|
|
72
|
-
require "rubygems/indexer"
|
|
73
|
-
require "geminabox"
|
|
74
|
-
|
|
75
|
-
Geminabox.data = #{gems_path.inspect}
|
|
76
|
-
Geminabox.rubygems_proxy = true
|
|
77
|
-
|
|
78
|
-
run Geminabox::Server
|
|
79
|
-
RUBY
|
|
80
|
-
|
|
81
|
-
File.write(config.config_ru_path, content)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_rackup_cmd(*extra)
|
|
85
|
-
["rackup", config.config_ru_path, "--host", "127.0.0.1", "-p", config.port.to_s, "-s", "puma", *extra]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def build_start_cmd
|
|
89
|
-
build_rackup_cmd("-D", "-P", config.pid_file)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def build_foreground_cmd
|
|
93
|
-
build_rackup_cmd
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def start_server
|
|
97
|
-
cmd = build_start_cmd
|
|
98
|
-
|
|
99
|
-
Dir.chdir(config.cache_dir) do
|
|
100
|
-
_stdout, stderr, status = Open3.capture3(*cmd)
|
|
101
|
-
|
|
102
|
-
raise ServerError, "Failed to start server:\n#{stderr}" unless status.success? || File.exist?(config.pid_file)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Wait for server to be ready
|
|
106
|
-
wait_for_server
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def start_server_foreground
|
|
110
|
-
Dir.chdir(config.cache_dir) do
|
|
111
|
-
system(*build_foreground_cmd)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
60
|
def wait_for_process_exit(pid)
|
|
116
61
|
10.times do
|
|
117
62
|
return unless process_alive?(pid)
|
|
@@ -121,26 +66,6 @@ module Gemkeeper
|
|
|
121
66
|
Process.kill("KILL", pid) if process_alive?(pid)
|
|
122
67
|
end
|
|
123
68
|
|
|
124
|
-
def wait_for_server(timeout: 10)
|
|
125
|
-
require "net/http"
|
|
126
|
-
|
|
127
|
-
uri = URI(config.geminabox_url)
|
|
128
|
-
(timeout / 0.5).ceil.times do
|
|
129
|
-
return true if server_responding?(uri)
|
|
130
|
-
|
|
131
|
-
sleep 0.5
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
raise ServerError, "Server failed to start within #{timeout} seconds"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def server_responding?(uri)
|
|
138
|
-
response = Net::HTTP.get_response(uri)
|
|
139
|
-
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
140
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError
|
|
141
|
-
false
|
|
142
|
-
end
|
|
143
|
-
|
|
144
69
|
def read_pid
|
|
145
70
|
pid_file = config.pid_file
|
|
146
71
|
return nil unless File.exist?(pid_file)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Gemkeeper
|
|
6
|
+
# Polls the server's HTTP endpoint until it responds or a timeout is reached.
|
|
7
|
+
class ServerReadinessProbe
|
|
8
|
+
def initialize(url)
|
|
9
|
+
@uri = URI(url)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def wait(timeout: 10)
|
|
13
|
+
(timeout / 0.5).ceil.times do
|
|
14
|
+
return true if responding?
|
|
15
|
+
|
|
16
|
+
sleep 0.5
|
|
17
|
+
end
|
|
18
|
+
raise ServerError, "Server failed to start within #{timeout} seconds"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def responding?
|
|
24
|
+
response = Net::HTTP.get_response(@uri)
|
|
25
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
26
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/gemkeeper/version.rb
CHANGED
data/lib/gemkeeper.rb
CHANGED
|
@@ -6,7 +6,14 @@ require_relative "gemkeeper/output"
|
|
|
6
6
|
require_relative "gemkeeper/configuration"
|
|
7
7
|
require_relative "gemkeeper/lockfile_parser"
|
|
8
8
|
require_relative "gemkeeper/manifest_reader"
|
|
9
|
+
require_relative "gemkeeper/manifest_serializer"
|
|
9
10
|
require_relative "gemkeeper/gem_repo_resolver"
|
|
11
|
+
require_relative "gemkeeper/manifest_builder"
|
|
12
|
+
require_relative "gemkeeper/manifest_validator"
|
|
13
|
+
require_relative "gemkeeper/bundler_mirror_configurator"
|
|
14
|
+
require_relative "gemkeeper/gem_syncer"
|
|
15
|
+
require_relative "gemkeeper/rackup_process"
|
|
16
|
+
require_relative "gemkeeper/server_readiness_probe"
|
|
10
17
|
require_relative "gemkeeper/config_generator"
|
|
11
18
|
require_relative "gemkeeper/git_repository"
|
|
12
19
|
require_relative "gemkeeper/gem_builder"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemkeeper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dan Brubaker Horst
|
|
@@ -95,8 +95,11 @@ files:
|
|
|
95
95
|
- README.md
|
|
96
96
|
- exe/gemkeeper
|
|
97
97
|
- lib/gemkeeper.rb
|
|
98
|
+
- lib/gemkeeper/bundler_mirror_configurator.rb
|
|
98
99
|
- lib/gemkeeper/cli.rb
|
|
99
100
|
- lib/gemkeeper/cli/commands/list.rb
|
|
101
|
+
- lib/gemkeeper/cli/commands/manifest/generate.rb
|
|
102
|
+
- lib/gemkeeper/cli/commands/manifest/validate.rb
|
|
100
103
|
- lib/gemkeeper/cli/commands/server/start.rb
|
|
101
104
|
- lib/gemkeeper/cli/commands/server/status.rb
|
|
102
105
|
- lib/gemkeeper/cli/commands/server/stop.rb
|
|
@@ -108,12 +111,18 @@ files:
|
|
|
108
111
|
- lib/gemkeeper/errors.rb
|
|
109
112
|
- lib/gemkeeper/gem_builder.rb
|
|
110
113
|
- lib/gemkeeper/gem_repo_resolver.rb
|
|
114
|
+
- lib/gemkeeper/gem_syncer.rb
|
|
111
115
|
- lib/gemkeeper/gem_uploader.rb
|
|
112
116
|
- lib/gemkeeper/git_repository.rb
|
|
113
117
|
- lib/gemkeeper/lockfile_parser.rb
|
|
118
|
+
- lib/gemkeeper/manifest_builder.rb
|
|
114
119
|
- lib/gemkeeper/manifest_reader.rb
|
|
120
|
+
- lib/gemkeeper/manifest_serializer.rb
|
|
121
|
+
- lib/gemkeeper/manifest_validator.rb
|
|
115
122
|
- lib/gemkeeper/output.rb
|
|
123
|
+
- lib/gemkeeper/rackup_process.rb
|
|
116
124
|
- lib/gemkeeper/server_manager.rb
|
|
125
|
+
- lib/gemkeeper/server_readiness_probe.rb
|
|
117
126
|
- lib/gemkeeper/version.rb
|
|
118
127
|
- mise.toml
|
|
119
128
|
- sig/gemkeeper.rbs
|