gemkeeper 0.1.0 → 0.2.1
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/CHANGELOG.md +26 -0
- data/README.md +249 -12
- data/exe/gemkeeper +7 -1
- data/lib/gemkeeper/cli/commands/setup.rb +137 -0
- data/lib/gemkeeper/cli/commands/sync.rb +115 -40
- data/lib/gemkeeper/cli.rb +1 -0
- data/lib/gemkeeper/configuration.rb +27 -2
- data/lib/gemkeeper/errors.rb +1 -0
- data/lib/gemkeeper/git_repository.rb +57 -18
- data/lib/gemkeeper/lockfile_parser.rb +58 -0
- data/lib/gemkeeper/manifest_reader.rb +45 -0
- data/lib/gemkeeper/output.rb +26 -0
- data/lib/gemkeeper/server_manager.rb +45 -44
- data/lib/gemkeeper/version.rb +1 -1
- data/lib/gemkeeper.rb +3 -0
- data/mise.toml +6 -0
- data/specs/20260518-154733-gemkeeper-contractor-support/implementation-summary.md +75 -0
- data/specs/20260518-154733-gemkeeper-contractor-support/spec.md +287 -0
- metadata +13 -12
- data/.env.example +0 -1
- data/.envrc +0 -3
- data/.rubocop.yml +0 -30
- data/AGENTS.md +0 -52
- data/CLAUDE.md +0 -1
- data/CODE_OF_CONDUCT.md +0 -132
- data/Makefile +0 -26
- data/Rakefile +0 -12
- data/gemkeeper.yml.example +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97154d493b89cc264c9de34cd0c15f3712d2e23a9757ec9fc749c436dee126d5
|
|
4
|
+
data.tar.gz: 4dba3a688a98ea40994b5979f5ef16df68ecad0cbc6fb5a8f2581abab40b0537
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4531fa55ac8d2f512ba602bee71752299b22e0243313ed1c3489bf6d5b9723f8d94d7a23da77a2fa2de8cd9587fac243f34fb899c3c11d43d5c6a77a05d99818
|
|
7
|
+
data.tar.gz: 85e9e466db67728d70ca25be6fec81a98717325021e5a8e38257e1b4d51fde54c91e9486e2284f0a81cd68f6bc74a9ba87d319e3c994e8567574e1de86c17c4d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-05-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- RuboCop offenses in sync command and executable (numeric predicate style, `delete_prefix`, parentheses, cyclomatic complexity).
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-05-19
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `version: latest` now caches by the resolved gemspec version rather than the string "latest" — re-running `sync` is a no-op if the tip hasn't changed.
|
|
14
|
+
- Explicit versions accept both `v`-prefixed (`v1.2.3`) and bare semver (`1.2.3`) tag formats; the cache key is normalized to bare semver in both cases.
|
|
15
|
+
- `gemkeeper --help` and `gemkeeper server --help` now exit 0.
|
|
16
|
+
- Port validation raises a clear error at config load time rather than failing mid-command.
|
|
17
|
+
- Running the `gemkeeper` executable directly (without `bundle exec`) no longer raises a load error.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Sync output uses ANSI colors (TTY only) to distinguish progress steps, skips, and failures.
|
|
22
|
+
- `sync` prints a summary line on completion: `Sync complete: 2 synced, 1 skipped (3 total)`.
|
|
23
|
+
- Version reading for `version: latest` follows `require_relative` in the gemspec and falls back to globbing `lib/**/version.rb`, covering the common pattern where the version lives in a separate constant file.
|
|
24
|
+
|
|
3
25
|
## [0.1.0] - 2026-01-29
|
|
4
26
|
|
|
5
27
|
- Initial release
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.2.1...HEAD
|
|
30
|
+
[0.2.1]: https://github.com/danhorst/gemkeeper/compare/0.2.0...0.2.1
|
|
31
|
+
[0.2.0]: https://github.com/danhorst/gemkeeper/compare/0.1.0...0.2.0
|
data/README.md
CHANGED
|
@@ -1,19 +1,256 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This project is an opinionated wrapper around [Gem in a Box][1] for managing private gem dependencies in an offline development environment.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
For a local service, you'll need to install [Ruby][2], ideally with a [version manager][3].
|
|
7
|
+
### Via RubyGems
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
gem install gemkeeper
|
|
11
|
+
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
### Via Homebrew (MacOS)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
brew tap danhorst/tap
|
|
17
|
+
brew install danhorst/tap/gemkeeper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Forumla: [`danhorst/homebrew-tap`][2]
|
|
21
|
+
|
|
22
|
+
## Workstation Setup
|
|
23
|
+
|
|
24
|
+
If you cannot reach your organization's private gem server, follow these steps to use gemkeeper as a local proxy.
|
|
25
|
+
|
|
26
|
+
**Prerequisites:** You must have HTTPS access to the internal gem repositories on GitHub.
|
|
27
|
+
Configure GitHub credentials before step 4 — see [GitHub authentication docs][3].
|
|
28
|
+
|
|
29
|
+
1. Install gemkeeper:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install gemkeeper
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. Install your org's gem manifest:
|
|
36
|
+
|
|
37
|
+
Your organization should provide a command or file that writes `~/.config/gemkeeper/manifest.yml`.
|
|
38
|
+
This manifest lists the internal gems and their GitHub URLs, and may include the private gem source URL used in step 6.
|
|
39
|
+
|
|
40
|
+
3. Generate a `gemkeeper.yml` for your project:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gemkeeper setup path/to/Gemfile.lock
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This reads the lockfile, cross-references the manifest, and writes a `gemkeeper.yml` in the current directory.
|
|
47
|
+
It also prints the `bundle config` command you will need in step 6.
|
|
48
|
+
|
|
49
|
+
4. Build and cache the internal gems:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gemkeeper sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
5. Start the local gem server:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gemkeeper server start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
6. Point Bundler at the local server using the command printed by step 3:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle config set --local mirror.<your-private-gem-source-url> http://localhost:9292
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Replace `<your-private-gem-source-url>` with the gem source URL declared in your `Gemfile` (the one that requires VPN or private credentials).
|
|
68
|
+
The mirror approach redirects gem resolution to your local Geminabox without modifying the committed `Gemfile` or `Gemfile.lock`.
|
|
69
|
+
Public gems are proxied from RubyGems.org automatically.
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
1. Create a configuration file at `~/.config/gemkeeper/config.yml`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
port: 9292
|
|
77
|
+
gems:
|
|
78
|
+
- repo: https://github.com/company/internal-gem
|
|
79
|
+
version: latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. Start the server:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
gemkeeper server start
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
3. Configure your Rails app to use the local gem server:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Gemfile
|
|
92
|
+
source "http://localhost:9292" do
|
|
93
|
+
gem "internal-gem"
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
4. Sync your gems:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
gemkeeper sync
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
Gemkeeper looks for configuration files in these locations (in order):
|
|
106
|
+
|
|
107
|
+
1. `./gemkeeper.yml` (current directory)
|
|
108
|
+
2. `~/.config/gemkeeper/config.yml`
|
|
109
|
+
3. `~/.gemkeeper.yml`
|
|
110
|
+
4. `/usr/local/etc/gemkeeper.yml` (Homebrew on Intel)
|
|
111
|
+
5. `/opt/homebrew/etc/gemkeeper.yml` (Homebrew on Apple Silicon)
|
|
112
|
+
|
|
113
|
+
### Configuration Options
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
# Port for the Geminabox server (default: 9292)
|
|
117
|
+
port: 9292
|
|
118
|
+
|
|
119
|
+
# Where to clone gem repositories (default: ./cache/repos)
|
|
120
|
+
repos_path: ./cache/repos
|
|
121
|
+
|
|
122
|
+
# Where to store built gems (default: ./cache/gems)
|
|
123
|
+
gems_path: ./cache/gems
|
|
124
|
+
|
|
125
|
+
# PID file location (default: ./cache/gemkeeper.pid)
|
|
126
|
+
pid_file: ./cache/gemkeeper.pid
|
|
127
|
+
|
|
128
|
+
# List of gems to manage
|
|
129
|
+
gems:
|
|
130
|
+
# HTTPS is recommended — works without SSH key setup (alternative: git@github.com:company/gem-one.git)
|
|
131
|
+
- repo: https://github.com/company/gem-one
|
|
132
|
+
version: latest # Use the latest commit on main/master; cached by resolved gemspec version
|
|
133
|
+
|
|
134
|
+
- repo: https://github.com/company/gem-two
|
|
135
|
+
version: v1.2.3 # Use a specific tag; both v-prefixed and bare semver accepted
|
|
136
|
+
|
|
137
|
+
- repo: https://github.com/company/gem-two
|
|
138
|
+
version: from_lockfile # Read version from the nearest Gemfile.lock
|
|
139
|
+
|
|
140
|
+
- repo: https://github.com/company/ruby-gem-three
|
|
141
|
+
name: gem-three # Override the gem name (strips "ruby-" prefix by default)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CLI Commands
|
|
145
|
+
|
|
146
|
+
### Server Management
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Start the server (daemonized)
|
|
150
|
+
gemkeeper server start
|
|
151
|
+
|
|
152
|
+
# Start in foreground (for services/debugging)
|
|
153
|
+
gemkeeper server start --foreground
|
|
154
|
+
gemkeeper server start -f
|
|
155
|
+
|
|
156
|
+
# Start on a specific port
|
|
157
|
+
gemkeeper server start --port 8080
|
|
158
|
+
|
|
159
|
+
# Stop the server
|
|
160
|
+
gemkeeper server stop
|
|
161
|
+
|
|
162
|
+
# Check server status
|
|
163
|
+
gemkeeper server status
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Project Setup
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Generate gemkeeper.yml from a Gemfile.lock and org manifest
|
|
170
|
+
gemkeeper setup path/to/Gemfile.lock
|
|
171
|
+
|
|
172
|
+
# Use a custom manifest path
|
|
173
|
+
gemkeeper setup path/to/Gemfile.lock --manifest ~/.config/myorg/manifest.yml
|
|
174
|
+
|
|
175
|
+
# Overwrite existing gemkeeper.yml entirely
|
|
176
|
+
gemkeeper setup path/to/Gemfile.lock --force
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Gem Synchronization
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Sync all configured gems
|
|
183
|
+
gemkeeper sync
|
|
184
|
+
|
|
185
|
+
# Sync a specific gem
|
|
186
|
+
gemkeeper sync internal-gem
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Other Commands
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# List cached gems
|
|
193
|
+
gemkeeper list
|
|
194
|
+
|
|
195
|
+
# Show version
|
|
196
|
+
gemkeeper version
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Global Options
|
|
200
|
+
|
|
201
|
+
All commands support:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
--config PATH # Use a specific config file
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Running as a Service
|
|
208
|
+
|
|
209
|
+
### Homebrew Services (macOS)
|
|
210
|
+
|
|
211
|
+
If installed via Homebrew:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Start and enable at login
|
|
215
|
+
brew services start gemkeeper
|
|
216
|
+
|
|
217
|
+
# Stop the service
|
|
218
|
+
brew services stop gemkeeper
|
|
219
|
+
|
|
220
|
+
# Check status
|
|
221
|
+
brew services info gemkeeper
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Manual Background Mode
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Start daemonized
|
|
228
|
+
gemkeeper server start
|
|
229
|
+
|
|
230
|
+
# Check if running
|
|
231
|
+
gemkeeper server status
|
|
232
|
+
|
|
233
|
+
# Stop
|
|
234
|
+
gemkeeper server stop
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## How It Works
|
|
238
|
+
|
|
239
|
+
1. **Clone/Pull**: Gemkeeper clones (or pulls) gem repositories to a local cache.
|
|
240
|
+
2. **Build**: Builds `.gem` files from the source at the specified version/tag.
|
|
241
|
+
3. **Upload**: Uploads built gems to a local Geminabox server.
|
|
242
|
+
4. **Proxy**: Geminabox proxies public gems from RubyGems.org, so you only need one gem source.
|
|
243
|
+
|
|
244
|
+
This lets you use a combination of public and private gems from a single gem source.
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
bundle install
|
|
250
|
+
bundle exec rake test # Run tests
|
|
251
|
+
bundle exec rubocop # Run linter
|
|
252
|
+
```
|
|
15
253
|
|
|
16
254
|
[1]: https://github.com/geminabox/geminabox
|
|
17
|
-
[2]: https://
|
|
18
|
-
[3]: https://
|
|
19
|
-
[4]: https://claude.com/product/claude-code
|
|
255
|
+
[2]: https://github.com/danhorst/homebrew-tap
|
|
256
|
+
[3]: https://docs.github.com/en/authentication
|
data/exe/gemkeeper
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
5
|
+
|
|
4
6
|
require "gemkeeper"
|
|
5
7
|
require "gemkeeper/cli"
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
begin
|
|
10
|
+
Dry::CLI.new(Gemkeeper::CLI).call
|
|
11
|
+
rescue SystemExit => e
|
|
12
|
+
exit(ARGV.include?("--help") || ARGV.include?("-h") ? 0 : e.status)
|
|
13
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Gemkeeper
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
class Setup < Dry::CLI::Command
|
|
9
|
+
desc "Generate gemkeeper.yml from a Gemfile.lock and org manifest"
|
|
10
|
+
|
|
11
|
+
argument :lockfile_path, type: :string, required: true,
|
|
12
|
+
desc: "Path to the project's Gemfile.lock"
|
|
13
|
+
option :manifest, type: :string,
|
|
14
|
+
desc: "Path to gem manifest (default: ~/.config/gemkeeper/manifest.yml)"
|
|
15
|
+
option :config, type: :string, desc: "Path to write gemkeeper.yml (default: ./gemkeeper.yml)"
|
|
16
|
+
option :force, type: :boolean, default: false,
|
|
17
|
+
desc: "Overwrite existing gemkeeper.yml entirely"
|
|
18
|
+
|
|
19
|
+
def call(lockfile_path:, **options)
|
|
20
|
+
manifest_path = options[:manifest] || ManifestReader::DEFAULT_PATH
|
|
21
|
+
output_path = options[:config] || File.join(Dir.pwd, Configuration::DEFAULT_CONFIG_FILENAME)
|
|
22
|
+
|
|
23
|
+
manifest = ManifestReader.load(manifest_path)
|
|
24
|
+
lockfile_versions = LockfileParser.parse(lockfile_path)
|
|
25
|
+
|
|
26
|
+
matched = match_gems(manifest, lockfile_versions)
|
|
27
|
+
|
|
28
|
+
write_config(matched, output_path, force: options[:force])
|
|
29
|
+
print_bundler_instructions(output_path, manifest)
|
|
30
|
+
rescue ManifestNotFoundError => e
|
|
31
|
+
warn "Error: #{e.message}"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def match_gems(manifest, lockfile_versions)
|
|
38
|
+
matched = manifest.gems.filter_map do |gem_entry|
|
|
39
|
+
name = gem_entry[:name]
|
|
40
|
+
{ name: name, repo: gem_entry[:repo] } if lockfile_versions.key?(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
warn_unmatched_internals(manifest, lockfile_versions)
|
|
44
|
+
matched
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def warn_unmatched_internals(manifest, lockfile_versions)
|
|
48
|
+
lockfile_versions.each_key do |gem_name|
|
|
49
|
+
next if manifest.find_by_name(gem_name)
|
|
50
|
+
|
|
51
|
+
gem_prefix = gem_name.split("-").first
|
|
52
|
+
next unless manifest.gem_names.any? { |manifest_name| manifest_name.split("-").first == gem_prefix }
|
|
53
|
+
|
|
54
|
+
warn "Warning: #{gem_name} matches an internal name pattern but is not in the manifest — skipping"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write_config(matched_gems, output_path, force:)
|
|
59
|
+
existing = load_existing_config(output_path) unless force
|
|
60
|
+
existing ||= {}
|
|
61
|
+
|
|
62
|
+
gem_entries = matched_gems.map do |gem_entry|
|
|
63
|
+
{ "repo" => gem_entry[:repo], "version" => "from_lockfile" }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
config = if force || existing.empty?
|
|
67
|
+
build_fresh_config(gem_entries)
|
|
68
|
+
else
|
|
69
|
+
merge_config(existing, gem_entries, matched_gems)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
File.write(output_path, config.to_yaml)
|
|
73
|
+
puts "Wrote #{output_path}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def load_existing_config(path)
|
|
77
|
+
return nil unless File.exist?(path)
|
|
78
|
+
|
|
79
|
+
YAML.safe_load_file(path, permitted_classes: [], symbolize_names: false) || {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_fresh_config(gem_entries)
|
|
83
|
+
{
|
|
84
|
+
"port" => Configuration::DEFAULT_PORT,
|
|
85
|
+
"repos_path" => "./cache/repos",
|
|
86
|
+
"gems_path" => "./cache/gems",
|
|
87
|
+
"gems" => gem_entries
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def merge_config(existing, _new_gem_entries, matched_gems)
|
|
92
|
+
existing_gems = existing["gems"] || []
|
|
93
|
+
matched_names = matched_gems.map { |gem_entry| gem_entry[:name] }
|
|
94
|
+
|
|
95
|
+
# Build a lookup for new entries by repo
|
|
96
|
+
new_by_name = matched_gems.to_h do |gem_entry|
|
|
97
|
+
entry_name = gem_entry[:name]
|
|
98
|
+
[entry_name, { "repo" => gem_entry[:repo], "version" => "from_lockfile" }]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Update existing entries for matched gems, keep others untouched
|
|
102
|
+
updated = existing_gems.map do |entry|
|
|
103
|
+
repo = entry["repo"].to_s
|
|
104
|
+
name = File.basename(repo, ".git").sub(/^ruby-/, "")
|
|
105
|
+
if matched_names.include?(name)
|
|
106
|
+
new_by_name.delete(name).merge(entry.except("version")).merge("version" => "from_lockfile")
|
|
107
|
+
else
|
|
108
|
+
entry
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Append any matched gems not already in the config
|
|
113
|
+
updated += new_by_name.values
|
|
114
|
+
|
|
115
|
+
existing.merge("gems" => updated)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def print_bundler_instructions(config_path, manifest)
|
|
119
|
+
config = load_existing_config(config_path) || {}
|
|
120
|
+
port = config.fetch("port", Configuration::DEFAULT_PORT)
|
|
121
|
+
local_url = "http://localhost:#{port}"
|
|
122
|
+
source_url = manifest.source_url
|
|
123
|
+
puts ""
|
|
124
|
+
puts "To point Bundler at your local Geminabox, run:"
|
|
125
|
+
if source_url
|
|
126
|
+
puts " bundle config set --local mirror.#{source_url} #{local_url}"
|
|
127
|
+
else
|
|
128
|
+
puts " bundle config set --local mirror.<your-private-gem-source-url> #{local_url}"
|
|
129
|
+
puts " (Replace <your-private-gem-source-url> with the gem source URL from your Gemfile)"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
register "setup", Commands::Setup
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -11,73 +11,148 @@ module Gemkeeper
|
|
|
11
11
|
|
|
12
12
|
def call(gem_name: nil, **options)
|
|
13
13
|
config = Configuration.load(options[:config])
|
|
14
|
+
gems_to_sync = select_gems(config, gem_name)
|
|
15
|
+
uploader = GemUploader.new(config.geminabox_url)
|
|
16
|
+
counts, failures = run_sync(gems_to_sync, config, uploader)
|
|
17
|
+
report_results(counts, failures, gems_to_sync.size)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
def select_gems(config, gem_name)
|
|
23
|
+
all_gems = config.gems
|
|
24
|
+
if all_gems.empty?
|
|
16
25
|
warn "No gems configured. Add gems to your gemkeeper.yml file."
|
|
17
26
|
exit 1
|
|
18
27
|
end
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
config.gems.select { |g| g.name == gem_name }
|
|
22
|
-
else
|
|
23
|
-
config.gems
|
|
24
|
-
end
|
|
29
|
+
gems = gem_name ? all_gems.select { |gem| gem.name == gem_name } : all_gems
|
|
25
30
|
|
|
26
|
-
if
|
|
31
|
+
if gems.empty?
|
|
27
32
|
warn "No matching gem found: #{gem_name}"
|
|
28
33
|
exit 1
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
gems
|
|
37
|
+
end
|
|
32
38
|
|
|
39
|
+
def run_sync(gems_to_sync, config, uploader)
|
|
40
|
+
counts = { synced: 0, skipped: 0 }
|
|
41
|
+
failures = []
|
|
33
42
|
gems_to_sync.each do |gem_def|
|
|
34
|
-
sync_gem(gem_def, config, uploader)
|
|
43
|
+
result = sync_gem(gem_def, config, uploader)
|
|
44
|
+
counts[result] += 1
|
|
45
|
+
rescue Error => e
|
|
46
|
+
failures << { name: gem_def.name, message: e.message }
|
|
35
47
|
end
|
|
36
|
-
|
|
37
|
-
warn "Error: #{e.message}"
|
|
38
|
-
exit 1
|
|
48
|
+
[counts, failures]
|
|
39
49
|
end
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
def report_results(counts, failures, total)
|
|
52
|
+
parts = []
|
|
53
|
+
parts << Output.colorize("#{counts[:synced]} synced", :green) if counts[:synced].positive?
|
|
54
|
+
parts << Output.colorize("#{counts[:skipped]} skipped", :yellow) if counts[:skipped].positive?
|
|
55
|
+
parts << Output.colorize("#{failures.size} failed", :red) if failures.any?
|
|
56
|
+
puts "\nSync complete: #{parts.join(", ")} (#{total} total)"
|
|
57
|
+
|
|
58
|
+
return if failures.empty?
|
|
59
|
+
|
|
60
|
+
warn "\nSync completed with #{failures.size} failure(s):"
|
|
61
|
+
failures.each { |f| warn " #{f[:name]}: #{f[:message]}" }
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
|
42
64
|
|
|
43
65
|
def sync_gem(gem_def, config, uploader)
|
|
44
|
-
|
|
66
|
+
name = gem_def.name
|
|
67
|
+
repo_url = gem_def.repo
|
|
68
|
+
gems_path = config.gems_path
|
|
69
|
+
version = resolve_version(gem_def)
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
local_path = File.join(config.repos_path, gem_def.name)
|
|
48
|
-
repo = GitRepository.new(gem_def.repo, local_path)
|
|
71
|
+
return :skipped if !gem_def.latest? && cached?(name, version, gems_path)
|
|
49
72
|
|
|
50
|
-
puts "
|
|
51
|
-
|
|
73
|
+
puts "Syncing #{name} @ #{version}..."
|
|
74
|
+
|
|
75
|
+
local_path = File.join(config.repos_path, name)
|
|
76
|
+
repo = GitRepository.new(repo_url, local_path)
|
|
77
|
+
|
|
78
|
+
Output.step("Fetching from #{repo_url}...")
|
|
79
|
+
fetch_repo(repo, repo_url)
|
|
80
|
+
|
|
81
|
+
checkout_gem_version(repo, version)
|
|
52
82
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
if gem_def.latest?
|
|
84
|
+
version = repo.current_version or
|
|
85
|
+
raise BuildError, "Could not read version from gemspec in #{repo_url}"
|
|
86
|
+
return :skipped if cached?(name, version, gems_path)
|
|
87
|
+
end
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
builder = GemBuilder.new(local_path)
|
|
60
|
-
gem_path = builder.build
|
|
89
|
+
Output.step("Building gem...")
|
|
90
|
+
gem_path = GemBuilder.new(local_path, config.gems_path).build
|
|
61
91
|
|
|
62
|
-
|
|
63
|
-
puts " Uploading to Geminabox..."
|
|
92
|
+
Output.step("Uploading to Geminabox...")
|
|
64
93
|
result = uploader.upload(gem_path)
|
|
94
|
+
Output.step(result[:message])
|
|
95
|
+
Output.success(" Done!")
|
|
96
|
+
:synced
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_version(gem_def)
|
|
100
|
+
return gem_def.version unless gem_def.from_lockfile?
|
|
101
|
+
|
|
102
|
+
name = gem_def.name
|
|
103
|
+
lockfile_path = LockfileParser.find
|
|
104
|
+
unless lockfile_path
|
|
105
|
+
raise GitError,
|
|
106
|
+
"version: from_lockfile for #{name} — no Gemfile.lock found in " \
|
|
107
|
+
"#{Dir.pwd} or any parent directory"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
versions = LockfileParser.parse(lockfile_path)
|
|
111
|
+
version = versions[name]
|
|
112
|
+
raise GitError, "#{name} not found in #{lockfile_path}" unless version
|
|
65
113
|
|
|
66
|
-
|
|
114
|
+
version
|
|
115
|
+
end
|
|
67
116
|
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
def cached?(name, version, gems_path)
|
|
118
|
+
bare_version = version.delete_prefix("v")
|
|
119
|
+
gem_file = File.join(gems_path, "gems", "#{name}-#{bare_version}.gem")
|
|
120
|
+
if File.exist?(gem_file)
|
|
121
|
+
Output.skip("Skipping #{name} @ #{bare_version} (already cached)")
|
|
122
|
+
true
|
|
123
|
+
else
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
70
127
|
|
|
71
|
-
|
|
128
|
+
def fetch_repo(repo, repo_url)
|
|
129
|
+
repo.clone_or_pull
|
|
72
130
|
rescue GitError => e
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
raise auth_error?(e) ? auth_failure_error(repo_url, e) : e
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def checkout_gem_version(repo, version)
|
|
135
|
+
Output.step("Checking out #{version}...")
|
|
136
|
+
repo.checkout_version(version)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def auth_error?(error)
|
|
140
|
+
auth_patterns = [
|
|
141
|
+
/authentication failed/i,
|
|
142
|
+
/could not read from remote repository/i,
|
|
143
|
+
/permission denied \(publickey\)/i,
|
|
144
|
+
/repository not found/i,
|
|
145
|
+
/fatal: credential/i
|
|
146
|
+
]
|
|
147
|
+
auth_patterns.any? { |pat| error.message.match?(pat) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def auth_failure_error(repo_url, original_error)
|
|
151
|
+
GitError.new(
|
|
152
|
+
"Git authentication failed for #{repo_url}.\n" \
|
|
153
|
+
"#{original_error.message}\n" \
|
|
154
|
+
"Configure GitHub credentials: https://docs.github.com/en/authentication"
|
|
155
|
+
)
|
|
81
156
|
end
|
|
82
157
|
end
|
|
83
158
|
end
|
data/lib/gemkeeper/cli.rb
CHANGED