ace-lab 0.15.1 → 0.16.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: d44134fc91df559a8aee637c930a157dbe954bd14aef96efacf6214dfa3de1a6
4
- data.tar.gz: 990680d9bd014801d273b8a2fd3bb129d749b77b13f4ca4976de50645021c2dc
3
+ metadata.gz: 78e9b81907693f3c427e477a6f2a17052d1e72c9f13bb3b0fd505854d9c6e222
4
+ data.tar.gz: beac210464ec9f958652ad7b817a6c6452e90dca7acf320b0c652a957bb2e0bc
5
5
  SHA512:
6
- metadata.gz: f852af7d794bdc50ec4935a99d60d07e45cb39edeed1aae90877111f52e904c0ee110d994c21e23448611a3237c9789c7a039b2d419fb3d949babd3104ccb63c
7
- data.tar.gz: 66c4795ca86d7a58da5fbd51f27bc6d1f8c3f098c8dda230be258e31ef8418b0b312a4aff1cc0491efe8db72eb9ab4b049e9644f1420704ac7cee2495bbab307
6
+ metadata.gz: 07de910a64b118ae1335c7e220fe5d21b9cba2b82050f170d6094297d507cb57a20e87f3af76a7ad66f9d5607889b06c462433e430cb76d38c4ebe669ef8d5f2
7
+ data.tar.gz: 7518f23fc8caefbfd2bd813887d18716898aaee27c80078131ee0ca8a4c902e3d5538d0bb0bdcc4827731fa46eb16c99c587df701041440f5808104fc0c44295
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0] - 2026-11-11
11
+
12
+ ### Changed
13
+
14
+ - Install the complete runtime with one Bundler invocation and a persistent Gemfile/lockfile; restore the previous bundle inputs after a failed update. Keep diagnostics explicit, verify locked activation, and expose configured executable links.
15
+
10
16
  ## [0.15.1] - 2026-11-11
11
17
 
12
18
  ### Fixed
@@ -0,0 +1,43 @@
1
+ # Installed lab runtime
2
+
3
+ The lab's operational ACE bundle is separate from every project checkout and its
4
+ Gemfile. RubyGems/Bundler owns dependency resolution. `ace-lab install` performs
5
+ one `bundle install` for the declared gem set; success does not trigger a second
6
+ installation audit. `ace-lab verify` is an explicit diagnostic command.
7
+
8
+ Example for Ruby 3.4:
9
+
10
+ ```yaml
11
+ gemset:
12
+ gem_home: /opt/ace/runtime/bundle/ruby/3.4.0
13
+ bin_dir: /usr/local/bin
14
+ gems:
15
+ - name: ace-lab
16
+ version: 0.16.0
17
+ - name: ace-herdr
18
+ version: 0.11.0
19
+ - name: ace-overseer
20
+ version: 0.30.0
21
+ ```
22
+
23
+ `gem_home` is Bundler's standard `<bundle-root>/<ruby-engine>/<ruby-ABI>`
24
+ installation directory. The installer writes one Gemfile and Bundler configuration
25
+ at `<bundle-root>`, retaining the lockfile across updates. Bundler generates
26
+ binstubs with the installing Ruby's absolute shebang. Optional `bin_dir` exposes
27
+ those binstubs through normal executable links. Unrelated executable paths are
28
+ not overwritten.
29
+
30
+ A failed update restores the previous Gemfile, lockfile and bundle configuration.
31
+ Already installed sibling versions remain on disk; the lockfile selects the
32
+ runtime. Diagnostics of a bundled runtime use frozen, read-only `bundle check`.
33
+
34
+ For updates, change the manifest pins and run `ace-lab install` once. To rerun
35
+ installation of an unchanged runtime directly:
36
+
37
+ ```sh
38
+ cd /opt/ace/runtime/bundle
39
+ bundle install
40
+ ```
41
+
42
+ No project Gemfile is changed. Native Herdr, Node, Pi and Forgejo are provisioned
43
+ separately; a gem update does not reinstall the VM or recreate its repositories.
@@ -31,7 +31,7 @@ module Ace
31
31
  DEFAULT_MODE = "0755"
32
32
  DEFAULT_KEY_TYPE = "ed25519"
33
33
  KEY_TYPES = [DEFAULT_KEY_TYPE].freeze
34
- GEMSET_KEYS = %w[gem_home gems extensions].freeze
34
+ GEMSET_KEYS = %w[gem_home gems extensions bin_dir].freeze
35
35
  GEM_KEYS = %w[name version].freeze
36
36
  GEM_NAME_PATTERN = /\A[A-Za-z0-9._-]+\z/
37
37
  EXTENSION_KEYS = %w[gem path account].freeze
@@ -420,8 +420,14 @@ module Ace
420
420
  raise_invalid "gemset gem_home is required and must be an absolute path"
421
421
  end
422
422
 
423
- {"gem_home" => gem_home.strip, "gems" => normalize_gems(raw["gems"]),
424
- "extensions" => normalize_extensions(raw["extensions"], raw["gems"], usernames)}
423
+ normalized = {"gem_home" => gem_home.strip, "gems" => normalize_gems(raw["gems"]),
424
+ "extensions" => normalize_extensions(raw["extensions"], raw["gems"], usernames)}
425
+ if raw.key?("bin_dir")
426
+ bin_dir = required_string(raw, "gemset", "bin_dir")
427
+ raise_invalid "gemset bin_dir must be an absolute path" unless absolute_path?(bin_dir)
428
+ normalized["bin_dir"] = bin_dir
429
+ end
430
+ normalized
425
431
  end
426
432
 
427
433
  # Harness extension projections: each entry names a DECLARED
@@ -103,7 +103,7 @@ module Ace
103
103
  # Final install verification stage: the readiness verdict with
104
104
  # every failed check named (adapters and tools included).
105
105
  def render_verify(verify_report)
106
- return ["Verify: skipped (#{verify_report['message']})"] if verify_report["status"] == "skipped"
106
+ return ["Verify: skipped (#{verify_report['message']})"] if %w[skipped not_run].include?(verify_report["status"])
107
107
 
108
108
  counts = verify_report["counts"] || {}
109
109
  verdict = verify_report["ready"] ? "ready" : "NOT READY"
@@ -3,30 +3,15 @@
3
3
  require_relative "../errors"
4
4
  require "open3"
5
5
  require "fileutils"
6
- require_relative "gem_payload_permissions"
6
+ require "yaml"
7
+ require "rbconfig"
7
8
 
8
9
  module Ace
9
10
  module Lab
10
11
  module Molecules
11
- # Idempotently converges the install manifest's declared gem set
12
- # into the declared target runtime (GEM_HOME; gad.2 gap #1, owner
13
- # spec 8xg.t.sbz.7): a gem whose exact declared version is already
14
- # present is a no-op (`exists`), a missing gem is installed
15
- # (`installed`), a gem present at a different version is updated
16
- # (the declared version installed alongside — RubyGems keeps
17
- # versions side by side; verify judges the declared set). Probes
18
- # and installs run the `gem` executable with GEM_HOME/GEM_PATH
19
- # pinned to the target runtime through the SAME argv runner seam
20
- # the other prepare molecules use (POSIX `env`, never a shell
21
- # string, ADR-031) — so the lab runtime stays isolated from the
22
- # installer's own gems and the runner stays injectable for
23
- # dry-run and tests. The runtime directory itself is created only
24
- # with administrator privileges (the same seam
25
- # AccountProvisioner gates creation with) and a symlink or
26
- # non-directory at the declared path is refused (the
27
- # DirectoryProvisioner takeover posture). Every failure is a
28
- # classified per-gem item with an actionable message; command
29
- # failures surface only the exit status and the first stderr line.
12
+ # Install the declared runtime as one Bundler bundle. Dependency
13
+ # resolution belongs to Bundler; read-only probes below are used only
14
+ # by explicit diagnostics.
30
15
  class GemsetProvisioner
31
16
  attr_reader :runner, :privileged, :dry_run
32
17
 
@@ -46,20 +31,71 @@ module Ace
46
31
 
47
32
  gem_home = runtime["gem_home"]
48
33
  runtime_failure = ensure_runtime(gem_home)
34
+ if runtime_failure
35
+ return gems.map { |gem| base(gem, gem_home).merge("status" => "failed", "message" => runtime_failure) }
36
+ end
37
+ if dry_run
38
+ return gems.map { |gem| base(gem, gem_home).merge("status" => "would_install", "message" => nil) }
39
+ end
40
+
41
+ scope = File.join(RUBY_ENGINE, RbConfig::CONFIG.fetch("ruby_version"))
42
+ unless gem_home.end_with?("/#{scope}")
43
+ raise Ace::Lab::InvalidConfigurationError,
44
+ "gemset gem_home must end in /#{scope} for the Bundler runtime"
45
+ end
46
+ bundle_root = File.dirname(File.dirname(gem_home))
47
+ gemfile = File.join(bundle_root, "Gemfile")
48
+ raise Ace::Lab::InvalidConfigurationError, "runtime Gemfile must not be a symlink" if File.symlink?(gemfile)
49
+ content = "source #{Gem.sources.first.to_s.dump}\n" + gems.map do |gem|
50
+ "gem #{gem.fetch('name').dump}, #{gem.fetch('version').dump}\n"
51
+ end.join
52
+ settings_dir = File.join(bundle_root, ".bundle")
53
+ settings_path = File.join(settings_dir, "config")
54
+ if File.symlink?(settings_dir) || File.symlink?(settings_path) || File.symlink?("#{gemfile}.lock")
55
+ raise Ace::Lab::InvalidConfigurationError, "runtime bundle configuration must not be a symlink"
56
+ end
57
+ previous = [gemfile, "#{gemfile}.lock", settings_path].to_h do |path|
58
+ [path, File.file?(path) ? [File.binread(path), File.stat(path).mode & 0o777] : nil]
59
+ end
60
+ begin
61
+ File.write(gemfile, content) unless File.file?(gemfile) && File.read(gemfile) == content
62
+ FileUtils.mkdir_p(settings_dir)
63
+ File.write(settings_path, YAML.dump(
64
+ "BUNDLE_PATH" => bundle_root, "BUNDLE_BIN" => File.join(gem_home, "bin"),
65
+ "BUNDLE_SHEBANG" => Gem.ruby))
66
+ result = @runner.call(runtime_argv(gem_home,
67
+ ["BUNDLE_GEMFILE=#{gemfile}", "BUNDLE_USER_CONFIG=#{File::NULL}", "bundle", "install"]))
68
+ failure = command_failure("bundle install", result) unless result["status"] == "ok"
69
+ install_entrypoints(gem_home, runtime["bin_dir"]) unless failure
70
+ rescue StandardError, Interrupt
71
+ restore_bundle(previous)
72
+ raise
73
+ end
74
+ restore_bundle(previous) if failure
49
75
  gems.map do |gem|
50
- if runtime_failure
51
- base(gem, gem_home).merge("status" => "failed", "message" => runtime_failure)
52
- else
53
- report = converge(base(gem, gem_home), gem, gem_home)
54
- if %w[exists installed updated].include?(report["status"])
55
- converge_post_presence(report, gem, gem_home)
56
- else
57
- report
58
- end
59
- end
76
+ base(gem, gem_home).merge("status" => failure ? "failed" : "installed", "message" => failure)
60
77
  end
61
78
  end
62
79
 
80
+ # A configured bundle controls activation even when another version
81
+ # is installed beside it. Bundler owns checking its locked closure.
82
+ def bundle_status(name:, version:, gem_home:)
83
+ gemfile = File.join(File.dirname(File.dirname(gem_home)), "Gemfile")
84
+ return nil unless File.file?(gemfile)
85
+ lock = "#{gemfile}.lock"
86
+ return [:failed, "runtime bundle lock is missing; rerun ace-lab install"] unless File.file?(lock)
87
+ require "bundler"
88
+ specs = Bundler::LockfileParser.new(File.read(lock)).specs
89
+ unless specs.any? { |spec| spec.name == name && spec.version.to_s == version }
90
+ return [:failed, "runtime bundle does not lock #{name} at #{version}; rerun ace-lab install"]
91
+ end
92
+ result = @runner.call(runtime_argv(gem_home,
93
+ ["BUNDLE_GEMFILE=#{gemfile}", "BUNDLE_USER_CONFIG=#{File::NULL}", "BUNDLE_FROZEN=true", "bundle", "check", "--dry-run"]))
94
+ result["status"] == "ok" ? [:ok, nil] : [:failed, command_failure("bundle check", result)]
95
+ rescue Bundler::BundlerError, SystemCallError => error
96
+ [:failed, "runtime bundle is unavailable (#{error.class}); rerun ace-lab install"]
97
+ end
98
+
63
99
  # Public READ-ONLY probe (L8.5): the local versions of `name`
64
100
  # inside the target runtime — the exact probe convergence runs,
65
101
  # exposed so the readiness verifier reuses the same logic
@@ -135,6 +171,31 @@ module Ace
135
171
 
136
172
  private
137
173
 
174
+ def restore_bundle(previous)
175
+ previous.each do |path, saved|
176
+ if saved
177
+ File.binwrite(path, saved.first)
178
+ File.chmod(saved.last, path)
179
+ else
180
+ File.unlink(path) if File.file?(path)
181
+ end
182
+ end
183
+ end
184
+
185
+ def install_entrypoints(gem_home, directory)
186
+ return unless directory
187
+ FileUtils.mkdir_p(directory)
188
+ Dir.glob(File.join(gem_home, "bin", "ace-*")).each do |source|
189
+ target = File.join(directory, File.basename(source))
190
+ if File.symlink?(target)
191
+ next if File.expand_path(File.readlink(target), directory) == source
192
+ raise Ace::Lab::InvalidConfigurationError, "executable already belongs to another runtime: #{target}"
193
+ end
194
+ raise Ace::Lab::InvalidConfigurationError, "executable already exists: #{target}" if File.exist?(target)
195
+ File.symlink(source, target)
196
+ end
197
+ end
198
+
138
199
  def base(gem, gem_home)
139
200
  {"name" => gem["name"], "version" => gem["version"], "gem_home" => gem_home}
140
201
  end
@@ -163,147 +224,6 @@ module Ace
163
224
  "gem runtime '#{gem_home}' could not be created (#{e.class.name}); rerun ace-lab prepare"
164
225
  end
165
226
 
166
- def converge(base, gem, gem_home)
167
- kind, versions_or_message = runtime_versions(name: gem["name"], gem_home: gem_home)
168
- # Prepare's fail-closed posture: an unprobeable runtime (no
169
- # `gem` executable — the :unknown probe kind) fails the item in
170
- # wet AND dry runs, exactly as the wet run would fail; verify
171
- # maps the same probe kind to its offline `unknown` posture.
172
- return base.merge("status" => "failed", "message" => versions_or_message) if kind != :ok
173
-
174
- versions = versions_or_message
175
- return base.merge("status" => "exists", "message" => nil) if versions.include?(gem["version"])
176
-
177
- status = versions.empty? ? "install" : "update"
178
- return base.merge("status" => "would_#{status}", "message" => nil) if @dry_run
179
-
180
- failure = install(gem, gem_home)
181
- return base.merge("status" => "failed", "message" => failure) if failure
182
-
183
- if status == "install"
184
- base.merge("status" => "installed", "message" => nil)
185
- else
186
- base.merge("status" => "updated", "message" => nil)
187
- end
188
- end
189
-
190
- # Post-presence convergence (task 8xs.t.pu0.0): the declared
191
- # version being present is necessary but not sufficient.
192
- # 1. NEWER versions of the declared gem sitting beside the
193
- # declared one would win RubyGems activation and take over
194
- # the runtime's CLI — the manifest is the single owner of
195
- # the declared set, so convergence removes them from the
196
- # target runtime.
197
- # 2. The dependency closure must actually resolve in the
198
- # runtime: a gem whose version already figures in
199
- # `gem list` can still miss a transitive runtime
200
- # dependency (the manually-repaired `json` class of gap);
201
- # missing dependencies are converged by installing them
202
- # at the parent's requirement, and an unsatisfiable
203
- # dependency is a classified failure — never readiness.
204
- # A dry run predicts both without writing (would_uninstall /
205
- # would_install) and stops recursion at the prediction.
206
- def converge_post_presence(report, gem, gem_home)
207
- removal = converge_newer_versions(gem, gem_home)
208
- return report.merge("status" => "failed", "message" => removal) if removal.is_a?(String)
209
- report["removed_newer_versions"] = removal
210
-
211
- closure = converge_closure(gem, gem_home)
212
- return report.merge("status" => "failed", "message" => closure) if closure.is_a?(String)
213
- report["closure"] = closure
214
- permissions = GemPayloadPermissions.new(gem_home: gem_home, dry_run: @dry_run,
215
- privileged: @privileged.call).call(name: gem["name"], version: gem["version"])
216
- report["permissions"] = permissions
217
- report
218
- rescue GemPayloadPermissions::Error => e
219
- report.merge("status" => "failed", "message" => e.message)
220
- end
221
-
222
- # @return [Array<Hash>, String] per-version removal reports or a
223
- # classified failure message
224
- def converge_newer_versions(gem, gem_home)
225
- kind, versions_or_message = runtime_versions(name: gem["name"], gem_home: gem_home)
226
- return versions_or_message if kind != :ok
227
-
228
- declared = Gem::Version.new(gem["version"])
229
- newer = versions_or_message.select { |version| Gem::Version.new(version) > declared }
230
- return [] if newer.empty?
231
-
232
- return newer.map { |version| {"version" => version, "status" => "would_uninstall"} } if @dry_run
233
-
234
- newer.map do |version|
235
- result = @runner.call(runtime_argv(gem_home,
236
- ["gem", "uninstall", gem["name"], "--version", version, "-x", "--force"]))
237
- if result["status"] != "ok"
238
- break command_failure("gem uninstall #{gem["name"]} #{version}", result)
239
- end
240
- {"version" => version, "status" => "uninstalled"}
241
- end
242
- end
243
-
244
- # Breadth-first walk of the gem's RUNTIME dependency closure in
245
- # the target runtime. `gem dependency` output is plain text (no
246
- # marshaled objects), development dependencies are ignored.
247
- # @return [Array<Hash>, String] per-dependency convergence
248
- # actions or a classified failure message
249
- def converge_closure(gem, gem_home)
250
- queue = [[gem["name"], gem["version"]]]
251
- seen = {}
252
- actions = []
253
- until queue.empty?
254
- name, version = queue.shift
255
- next if seen[[name, version]]
256
-
257
- seen[[name, version]] = true
258
- deps = runtime_dependencies(name: name, version: version, gem_home: gem_home)
259
- return deps if deps.is_a?(String)
260
-
261
- deps.each do |dep|
262
- kind, versions_or_message = runtime_versions(name: dep["name"], gem_home: gem_home)
263
- return versions_or_message if kind != :ok
264
-
265
- satisfying = satisfying_versions(dep, versions_or_message)
266
- if satisfying.empty?
267
- if @dry_run
268
- actions << {"name" => dep["name"], "requirement" => dep["requirement"],
269
- "status" => "would_install"}
270
- next # recursion stops at the prediction
271
- end
272
-
273
- failure = install_dependency(dep, gem_home)
274
- return failure if failure
275
-
276
- kind, versions_or_message = runtime_versions(name: dep["name"], gem_home: gem_home)
277
- return versions_or_message if kind != :ok
278
- satisfying = satisfying_versions(dep, versions_or_message)
279
- if satisfying.empty?
280
- return "dependency '#{dep["name"]} (#{dep["requirement"]})' required by gem " \
281
- "'#{name}' is not satisfiable in runtime '#{gem_home}' after install; " \
282
- "check the gem source or the manifest declaration"
283
- end
284
- actions << {"name" => dep["name"], "requirement" => dep["requirement"],
285
- "status" => "installed"}
286
- end
287
-
288
- chosen = satisfying.max_by { |v| Gem::Version.new(v) }
289
- queue << [dep["name"], chosen]
290
- end
291
- end
292
- actions
293
- end
294
-
295
- # Installs a missing dependency at the parent requirement: the
296
- # requirement string the runtime's own resolver would accept
297
- # (`gem install name -v "~> 2.7"`).
298
- # @return [String, nil] classified failure, nil on success
299
- def install_dependency(dep, gem_home)
300
- result = @runner.call(runtime_argv(gem_home,
301
- ["gem", "install", dep["name"], "--version", dep["requirement"], "--no-document"]))
302
- return nil if result["status"] == "ok"
303
-
304
- command_failure("gem install #{dep["name"]}", result)
305
- end
306
-
307
227
  # The runtime (non-development) dependencies of an installed
308
228
  # gem, from `gem dependency <name> --version <v>` plain-text
309
229
  # output: "dep (~> 2.0, runtime)" lines under the gem header.
@@ -344,19 +264,6 @@ module Ace
344
264
  end
345
265
  end
346
266
 
347
- # Installs the EXACT declared version into the runtime. Sources
348
- # are the gem command's own defaults — fetching from the
349
- # configured gem source is provisioning network access, the same
350
- # envelope as cloning a declared repository remote.
351
- # @return [String, nil] the classified failure, nil on success
352
- def install(gem, gem_home)
353
- result = @runner.call(runtime_argv(gem_home,
354
- ["gem", "install", gem["name"], "--version", gem["version"], "--no-document"]))
355
- return nil if result["status"] == "ok"
356
-
357
- command_failure("gem install", result)
358
- end
359
-
360
267
  # Environment variables that would drag the INVOKING process's
361
268
  # bundler context into the gem subprocess: RUBYOPT carries
362
269
  # `-rbundler/setup` under `bundle exec ace-lab` (the documented
@@ -375,7 +282,7 @@ module Ace
375
282
  # sees ONLY the target runtime.
376
283
  def runtime_argv(gem_home, argv)
377
284
  ["env"] +
378
- BUNDLER_ENV_KEYS.flat_map { |key| ["-u", key] } +
285
+ (BUNDLER_ENV_KEYS + ENV.keys.grep(/\ABUNDLER?_/)).uniq.flat_map { |key| ["-u", key] } +
379
286
  ["GEM_HOME=#{gem_home}", "GEM_PATH=#{gem_home}"] + argv
380
287
  end
381
288
 
@@ -115,11 +115,11 @@ module Ace
115
115
  "(trusted grants are configured only for a converged install)"}
116
116
  end
117
117
 
118
- verify_report = verify_stage(manifest, effective_dry_run)
118
+ verify_report = {"ready" => nil, "status" => "not_run",
119
+ "message" => "Diagnostics are explicit: run ace-lab verify when needed."}
119
120
 
120
121
  {
121
- "ok" => prepare_ok && projects_ok && grants["status"] != "failed" &&
122
- verify_stage_ok?(verify_report),
122
+ "ok" => prepare_ok && projects_ok && grants["status"] != "failed",
123
123
  "dry_run" => effective_dry_run,
124
124
  "machine" => manifest["machine"],
125
125
  "manifest" => manifest_path,
@@ -135,35 +135,6 @@ module Ace
135
135
 
136
136
  private
137
137
 
138
- # Final install verification (LAB-3): read-only, over the
139
- # converged artifacts. A wet run's readiness gates `ok`; a dry
140
- # run has nothing converged to verify and reports the stage
141
- # skipped instead of a guaranteed failure.
142
- def verify_stage(manifest, dry_run)
143
- return {"ready" => nil, "status" => "skipped",
144
- "message" => "verification runs on the converged lab; dry-run predicts the " \
145
- "steps above only"} if dry_run
146
-
147
- verify_organism.call
148
- rescue Ace::Lab::InvalidConfigurationError => e
149
- {"ready" => false, "status" => "failed",
150
- "message" => "final verification could not run: #{e.message}"}
151
- rescue StandardError => e
152
- {"ready" => false, "status" => "failed",
153
- "message" => "final verification crashed: #{e.class}: #{e.message}"}
154
- end
155
-
156
- def verify_stage_ok?(report)
157
- report["ready"] != false
158
- end
159
-
160
- def verify_organism
161
- @verify || Verify.new(
162
- manifest_path: manifest_path, runner: @runner, account_lookup: @account_lookup,
163
- authorization_path: grants_molecule.authorization_path, platform: platform
164
- )
165
- end
166
-
167
138
  # The project's target executor account: the owner of the
168
139
  # declared directory the project lives in (longest prefix
169
140
  # match; a directory declared AS the exact project path matches
@@ -613,6 +613,9 @@ module Ace
613
613
  reason = gem_version_reason(gem, versions, gem_home)
614
614
  return [FAIL, reason] if reason
615
615
 
616
+ bundle = provisioner.bundle_status(name: gem["name"], version: gem["version"], gem_home: gem_home)
617
+ return [bundle.first == :ok ? PASS : FAIL, bundle.last] if bundle
618
+
616
619
  newer_kind, newer_or_message = provisioner.newer_versions(
617
620
  name: gem["name"], declared_version: gem["version"], gem_home: gem_home
618
621
  )
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Lab
5
- VERSION = "0.15.1"
5
+ VERSION = "0.16.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
@@ -9,6 +9,26 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 2026-11-11 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.6'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.6'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3'
12
32
  - !ruby/object:Gem::Dependency
13
33
  name: ace-support-cli
14
34
  requirement: !ruby/object:Gem::Requirement
@@ -85,20 +105,6 @@ dependencies:
85
105
  - - "~>"
86
106
  - !ruby/object:Gem::Version
87
107
  version: '0.14'
88
- - !ruby/object:Gem::Dependency
89
- name: bundler
90
- requirement: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - "~>"
93
- - !ruby/object:Gem::Version
94
- version: '2.0'
95
- type: :development
96
- prerelease: false
97
- version_requirements: !ruby/object:Gem::Requirement
98
- requirements:
99
- - - "~>"
100
- - !ruby/object:Gem::Version
101
- version: '2.0'
102
108
  - !ruby/object:Gem::Dependency
103
109
  name: minitest
104
110
  requirement: !ruby/object:Gem::Requirement
@@ -172,6 +178,7 @@ files:
172
178
  - LICENSE
173
179
  - README.md
174
180
  - Rakefile
181
+ - docs/runtime-bundle.md
175
182
  - docs/usage.md
176
183
  - exe/ace-lab
177
184
  - lib/ace/lab.rb