gemkeeper 0.6.7 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85fffab47c1459590a6dd86cd4522ca533a54837516d50d15b377e1e5a1145c7
4
- data.tar.gz: e52ee33dae0af8861b747400e284be4e4c847f28d297685490df29dfe0a1af6e
3
+ metadata.gz: bed56339bf29ed0dd82b9aec9c94a4a466b0d38364b3b3e5280d4d51b1101cf0
4
+ data.tar.gz: '09132dd1f425d64e0c423386583a29c9010b0f73fe016088428e5a3fe5dbe57c'
5
5
  SHA512:
6
- metadata.gz: 78143e09b2661e66bbc08cd37c986f0363a4ae2d24d1fc4e81a39c57cd0e4b7281827e2b53631ff6e7762b2491fc2ed02cfacdc4579b6680d9620a63317e3367
7
- data.tar.gz: 50dfb941e3611a175c81e3d0b99e33a41db486804f910fbd15a7c618c7d7bc794d7c5986426fde5b08b8467121d72c9134cbf813c3ffa02ee7470d54a9e52bc2
6
+ metadata.gz: 4e1178a59bda6fd8f299f1e16e68d828ae23b8176785dba7cff5c6c7f7d8776563e564fcadbb0746ea11bc8ce808a00be89de06836cf36dd49ecfbb52f401de4
7
+ data.tar.gz: 81e4e60ce2f0ced3d809448f4bbbada9a959bbc33215f27808c5eaf4a7987e56f6905259e26c93b1e09b177c503b5422702d7fb940f08855ae2e314c84146670
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2026-05-28
4
+
5
+ ### Changed
6
+
7
+ - `gemkeeper setup` and `gemkeeper manifest generate` no longer write `repo:` into generated `gemkeeper.yml` entries.
8
+ The repo URL is resolved from the manifest by gem name at sync time, making the manifest the single source of truth for name→repo mappings.
9
+ Re-running `setup` strips `repo:` from matched entries, promoting them to manifest-only.
10
+ - `gemkeeper setup` now accepts only a `Gemfile.lock`, `Gemfile`, or directory as its source.
11
+ The path that imported an existing `gemkeeper.yml` into the manifest has been removed — it only made sense when configs carried `repo:`; populate the manifest with `gemkeeper manifest generate` or `setup` from a lockfile instead.
12
+
13
+ ### Added
14
+
15
+ - `repo:` in `gemkeeper.yml` is now optional. When absent, `gemkeeper sync` resolves it from the manifest (`~/.config/gemkeeper/manifest.yml`) by gem name.
16
+ It remains a supported per-project override (escape hatch) for gems not in the manifest; `sync` warns when an explicit `repo:` diverges from the manifest and uses the `gemkeeper.yml` value.
17
+
3
18
  ## [0.6.7] - 2026-05-28
4
19
 
5
20
  ### Fixed
@@ -139,7 +154,8 @@
139
154
 
140
155
  - Initial release
141
156
 
142
- [Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.6.7...HEAD
157
+ [Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.7.0...HEAD
158
+ [0.7.0]: https://github.com/danhorst/gemkeeper/compare/0.6.7...0.7.0
143
159
  [0.6.7]: https://github.com/danhorst/gemkeeper/compare/0.6.6...0.6.7
144
160
  [0.6.6]: https://github.com/danhorst/gemkeeper/compare/0.6.5...0.6.6
145
161
  [0.6.5]: https://github.com/danhorst/gemkeeper/compare/0.6.4...0.6.5
data/README.md CHANGED
@@ -70,15 +70,17 @@ Public gems are proxied from RubyGems.org automatically.
70
70
 
71
71
  ## Quick Start
72
72
 
73
- 1. Create a configuration file at `~/.config/gemkeeper/config.yml`:
73
+ 1. Ensure your org manifest is present at `~/.config/gemkeeper/manifest.yml` (see [Workstation Setup](#workstation-setup)), then create a project `gemkeeper.yml`:
74
74
 
75
75
  ```yaml
76
76
  port: 9292
77
77
  gems:
78
- - repo: https://github.com/company/internal-gem
78
+ - name: internal-gem
79
79
  version: latest
80
80
  ```
81
81
 
82
+ The repo URL for `internal-gem` is resolved from the manifest at sync time.
83
+
82
84
  2. Start the server:
83
85
 
84
86
  ```bash
@@ -127,18 +129,21 @@ pid_file: ./cache/gemkeeper.pid
127
129
 
128
130
  # List of gems to manage
129
131
  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
+ # Preferred form: name only. The repo URL is resolved from the manifest at sync time.
133
+ - name: gem-one
132
134
  version: latest # Use the latest commit on main/master; cached by resolved gemspec version
133
135
 
134
- - repo: https://github.com/company/gem-two
136
+ - name: gem-two
135
137
  version: v1.2.3 # Use a specific tag; both v-prefixed and bare semver accepted
136
138
 
137
- - repo: https://github.com/company/gem-two
139
+ - name: gem-two
138
140
  version: from_lockfile # Read version from the nearest Gemfile.lock
139
141
 
140
- - repo: https://github.com/company/ruby-gem-three
141
- name: gem-three # Override the gem name (strips "ruby-" prefix by default)
142
+ # repo: is optional — a per-project override for gems not in the manifest (HTTPS recommended,
143
+ # works without SSH key setup; alternative: git@github.com:company/gem-three.git). When both are
144
+ # present, repo: wins and sync warns if it diverges from the manifest.
145
+ - name: gem-three
146
+ repo: https://github.com/company/ruby-gem-three
142
147
  ```
143
148
 
144
149
  ## CLI Commands
@@ -169,9 +174,6 @@ gemkeeper server status
169
174
  # Generate gemkeeper.yml from a Gemfile.lock and org manifest
170
175
  gemkeeper setup path/to/Gemfile.lock
171
176
 
172
- # Use an existing gemkeeper.yml as input (updates manifest, optionally installs as global config)
173
- gemkeeper setup path/to/gemkeeper.yml
174
-
175
177
  # Use a custom manifest path
176
178
  gemkeeper setup path/to/Gemfile.lock --manifest ~/.config/myorg/manifest.yml
177
179
 
@@ -195,7 +197,8 @@ It sets `repos_path` and `gems_path` as absolute paths under the corresponding `
195
197
  ### Manifest Management
196
198
 
197
199
  The manifest (`~/.config/gemkeeper/manifest.yml`) is the global name→repo lookup table shared across projects.
198
- `manifest generate` builds or updates it; `setup` reads it.
200
+ `manifest generate` builds or updates it; `setup` reads it, and `sync` resolves each gem's repo URL from it.
201
+ Because `gemkeeper.yml` entries omit `repo:`, the manifest must be present before `sync`; run `gemkeeper manifest validate --resolve` first to confirm every gem maps to a reachable repo.
199
202
 
200
203
  ```bash
201
204
  # Build or update the manifest from a Gemfile.lock
@@ -6,11 +6,10 @@ module Gemkeeper
6
6
  class Setup < Dry::CLI::Command
7
7
  include CLI::LockfileResolution
8
8
 
9
- desc "Generate gemkeeper.yml from a Gemfile.lock or existing gemkeeper.yml"
9
+ desc "Generate gemkeeper.yml from a Gemfile.lock"
10
10
 
11
11
  argument :source_path, type: :string, required: false,
12
- desc: "Gemfile.lock, Gemfile, directory, or gemkeeper.yml " \
13
- "(default: nearest Gemfile.lock)"
12
+ desc: "Gemfile.lock, Gemfile, or directory (default: nearest Gemfile.lock)"
14
13
  option :manifest, type: :string,
15
14
  desc: "Path to gem manifest (default: ~/.config/gemkeeper/manifest.yml)"
16
15
  option :config, type: :string, desc: "Path to write gemkeeper.yml (default: ./gemkeeper.yml)"
@@ -25,12 +24,9 @@ module Gemkeeper
25
24
  validate_options!(options)
26
25
  output_path = resolve_output_path(options)
27
26
  resolved = resolve_source_path(source_path)
27
+ not_a_lockfile!(resolved) unless lockfile?(resolved)
28
28
 
29
- if lockfile?(resolved)
30
- setup_from_lockfile(resolved, output_path, options)
31
- else
32
- setup_from_config(resolved, output_path, options)
33
- end
29
+ setup_from_lockfile(resolved, output_path, options)
34
30
  rescue UnresolvableGemError, ManifestConflictError => error
35
31
  warn "Error: #{error.message}"
36
32
  exit 1
@@ -61,36 +57,10 @@ module Gemkeeper
61
57
  BundlerMirrorConfigurator.new(resolved, port:, global: options[:global]).configure
62
58
  end
63
59
 
64
- def setup_from_config(source_path, output_path, options)
65
- source = YAML.safe_load_file(source_path, permitted_classes: [], symbolize_names: false) || {}
66
- manifest = load_manifest(options)
67
- update_manifest_from_config(source, manifest, options)
68
- install_global_config(source, output_path) if options[:global]
69
- end
70
-
71
- def update_manifest_from_config(source, manifest, options)
72
- (source["gems"] || []).each do |entry|
73
- repo = entry["repo"].to_s
74
- next if repo.empty?
75
-
76
- name = File.basename(repo, ".git").sub(/^ruby-/, "")
77
- manifest.add_mapping(name:, repo:)
78
- end
79
- manifest.save(manifest_path(options))
80
- end
81
-
82
- def install_global_config(source, output_path)
83
- existing = File.exist?(output_path) ? (YAML.safe_load_file(output_path) || {}) : {}
84
- merged = existing.merge(source.except("gems")).merge("gems" => merge_gem_lists(existing, source))
85
- File.write(output_path, merged.to_yaml)
86
- puts "Wrote #{output_path}"
87
- end
88
-
89
- def merge_gem_lists(existing, source)
90
- existing_gems = existing["gems"] || []
91
- source_gems = source["gems"] || []
92
- existing_repos = existing_gems.to_set { |g| g["repo"] }
93
- existing_gems + source_gems.reject { |g| existing_repos.include?(g["repo"]) }
60
+ def not_a_lockfile!(path)
61
+ warn "Error: setup builds from a Gemfile.lock, Gemfile, or directory got #{path}. " \
62
+ "To populate the manifest, run 'gemkeeper manifest generate'."
63
+ exit 1
94
64
  end
95
65
 
96
66
  def load_manifest(options)
@@ -12,7 +12,7 @@ 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
- syncer = GemSyncer.new(config, GemUploader.new(config.geminabox_url))
15
+ syncer = GemSyncer.new(config, GemUploader.new(config.geminabox_url), manifest: ManifestReader.load)
16
16
  counts, failures = run_sync(gems_to_sync, syncer)
17
17
  report_results(counts, failures, gems_to_sync.size)
18
18
  end
@@ -22,7 +22,7 @@ module Gemkeeper
22
22
  def matched_gems
23
23
  matched = @manifest.gems.filter_map do |gem_entry|
24
24
  name = gem_entry[:name]
25
- { name:, repo: gem_entry[:repo] } if @lockfile_versions.key?(name)
25
+ { name: } if @lockfile_versions.key?(name)
26
26
  end
27
27
  warn_unmatched
28
28
  matched
@@ -47,7 +47,7 @@ module Gemkeeper
47
47
 
48
48
  def build_fresh(matched, global_output_path: nil)
49
49
  repos_path, gems_path = data_paths_for(global_output_path)
50
- gem_entries = matched.map { |g| { "name" => g[:name], "repo" => g[:repo], "version" => "from_lockfile" } }
50
+ gem_entries = matched.map { |g| { "name" => g[:name], "version" => "from_lockfile" } }
51
51
  { "port" => Configuration::DEFAULT_PORT, "repos_path" => repos_path,
52
52
  "gems_path" => gems_path, "gems" => gem_entries }
53
53
  end
@@ -62,7 +62,7 @@ module Gemkeeper
62
62
  def merge(existing, matched)
63
63
  existing_gems = existing["gems"] || []
64
64
  new_by_name = matched.to_h do |g|
65
- [g[:name], { "name" => g[:name], "repo" => g[:repo], "version" => "from_lockfile" }]
65
+ [g[:name], { "name" => g[:name], "version" => "from_lockfile" }]
66
66
  end
67
67
 
68
68
  updated = existing_gems.map do |entry|
@@ -127,9 +127,11 @@ module Gemkeeper
127
127
  attr_reader :repo, :version, :name
128
128
 
129
129
  def initialize(config)
130
- @repo = config[:repo] or raise InvalidConfigError, "Gem definition missing 'repo'"
131
- @version = config[:version] || "latest"
130
+ @repo = config[:repo]
132
131
  @name = config[:name] || extract_name_from_repo
132
+ raise InvalidConfigError, "Gem definition needs a 'name' or 'repo'" unless @name
133
+
134
+ @version = config[:version] || "latest"
133
135
  validate_version!
134
136
  end
135
137
 
@@ -153,6 +155,8 @@ module Gemkeeper
153
155
  end
154
156
 
155
157
  def extract_name_from_repo
158
+ return nil unless @repo
159
+
156
160
  File.basename(@repo, ".git").sub(/^ruby-/, "")
157
161
  end
158
162
  end
@@ -11,12 +11,14 @@ module Gemkeeper
11
11
  /fatal: credential/i
12
12
  ].freeze
13
13
 
14
- def initialize(config, uploader)
14
+ def initialize(config, uploader, manifest:)
15
15
  @config = config
16
16
  @uploader = uploader
17
+ @manifest = manifest
17
18
  end
18
19
 
19
20
  def sync(gem_def)
21
+ repo_url = resolve_repo(gem_def)
20
22
  version = resolve_version(gem_def)
21
23
  name = gem_def.name
22
24
  gems_path = @config.gems_path
@@ -25,13 +27,13 @@ module Gemkeeper
25
27
 
26
28
  puts "Syncing #{name} @ #{version}..."
27
29
  local_path = File.join(@config.repos_path, name)
28
- repo = fetch_repo(gem_def.repo, local_path)
30
+ repo = fetch_repo(repo_url, local_path)
29
31
 
30
32
  Output.step("Checking out #{version}...")
31
33
  repo.checkout_version(version)
32
34
 
33
35
  if gem_def.latest?
34
- version = latest_version!(repo, name, gems_path, gem_def.repo)
36
+ version = latest_version!(repo, name, gems_path, repo_url)
35
37
  return :skipped unless version
36
38
  end
37
39
 
@@ -41,6 +43,33 @@ module Gemkeeper
41
43
 
42
44
  private
43
45
 
46
+ # Explicit repo: in gemkeeper.yml wins, but warns on divergence from the manifest.
47
+ # Otherwise the repo is resolved from the manifest by gem name.
48
+ def resolve_repo(gem_def)
49
+ manifest_repo = @manifest.repo_for(gem_def.name)
50
+ return manifest_repo || missing_repo!(gem_def.name) unless gem_def.repo
51
+
52
+ warn_if_divergent(gem_def.name, gem_def.repo, manifest_repo)
53
+ gem_def.repo
54
+ end
55
+
56
+ def missing_repo!(name)
57
+ unless File.exist?(@manifest.path)
58
+ raise InvalidConfigError,
59
+ "No manifest found at #{@manifest.path} — run 'gemkeeper manifest generate' to create one"
60
+ end
61
+
62
+ raise InvalidConfigError,
63
+ "No repo configured for #{name.inspect} — add it to the manifest with 'gemkeeper manifest generate'"
64
+ end
65
+
66
+ def warn_if_divergent(name, config_repo, manifest_repo)
67
+ return unless manifest_repo && manifest_repo != config_repo
68
+
69
+ warn "Warning: repo for #{name} in gemkeeper.yml (#{config_repo}) " \
70
+ "differs from manifest (#{manifest_repo}) — using gemkeeper.yml"
71
+ end
72
+
44
73
  def resolve_version(gem_def)
45
74
  return gem_def.version unless gem_def.from_lockfile?
46
75
 
@@ -5,7 +5,7 @@ module Gemkeeper
5
5
  class ManifestReader
6
6
  DEFAULT_PATH = File.expand_path("~/.config/gemkeeper/manifest.yml")
7
7
 
8
- attr_reader :gems, :source_url
8
+ attr_reader :gems, :source_url, :path
9
9
 
10
10
  def self.load(path = DEFAULT_PATH)
11
11
  new(path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemkeeper
4
- VERSION = "0.6.7"
4
+ VERSION = "0.7.0"
5
5
  end
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.6.7
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Brubaker Horst