dagger_ruby 0.9.0 → 0.11.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: 3236161fdcbdb34e426318363c0db9b1625fef0bfff6d83d021908ccaab26e69
4
- data.tar.gz: d0510f01bd92c27f6f06ba2c2693007771ffb8d6fb8da663ef2aea8798bd09d6
3
+ metadata.gz: ebaf58b886e0c88802e55c2abb2f10676c4b00466dfd24fc69d6f38ead563e6a
4
+ data.tar.gz: ad9abb4ee776703af437e713bf3d61a99b80e0f0d9586dadda2a98c689faedf7
5
5
  SHA512:
6
- metadata.gz: fb8c138bbf1c674ad47cffecb4d31c77348c83a4e83e6439f55240925587ba16035f53d21f49995d849807d6d1b9bd70edb0cabec3fe860c521efaade6307539
7
- data.tar.gz: ad20b3fb027215a665fb58fbfde7ae125d791e7b596c449ac23f91e87e07e16de154d9775b19d070066855b26cb062e8d69ffe382f64d9c59feab438daa225d6
6
+ metadata.gz: ac82debf94e095271f87339a9af2f82bc10fc3a07f049a343073c35effdb018c8d69e58cab7d446239303b4a0647e31cb69ea53e4f2193bf77c3bb53c3b676b4
7
+ data.tar.gz: ba23033d17a9803d92ba629d1cf4db2ae86b788081a54017cb4443a6122ab69c763f968bb7539429864771e720484a1c85008de5505da48575dcdff23ee76f53
data/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ All notable changes are recorded here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.0] - 2026-08-27
10
+
11
+ ### Added
12
+
13
+ - First-class `progress: :pretty` support for Docker-style application steps with live container output.
14
+ - Support for Dagger's `logs` and `dots` progress frontends.
15
+
16
+ ### Fixed
17
+
18
+ - Keep engine startup failures visible and reap the complete Dagger process group after completion or interruption.
19
+
20
+ ## [0.10.0] - 2026-08-25
21
+
22
+ ### Added
23
+
24
+ - Explicit Apple Container and Docker runner selection.
25
+ - Compatible Dagger CLI and engine verification before standalone sessions.
26
+ - Container default-argument, Docker healthcheck, and host image-store export APIs.
27
+
28
+ ### Changed
29
+
30
+ - Support Ruby 3.2 and newer so Ruby applications do not need Ruby 4 solely to
31
+ use the Dagger client.
32
+
9
33
  ## [0.9.0] - 2026-08-20
10
34
 
11
35
  ### Added
@@ -64,7 +88,9 @@ All notable changes are recorded here. The format follows
64
88
 
65
89
  - Initial container, directory, file, cache, secret, Git, and GraphQL client.
66
90
 
67
- [Unreleased]: https://github.com/boringcache/dagger_ruby/compare/v0.9.0...HEAD
91
+ [Unreleased]: https://github.com/boringcache/dagger_ruby/compare/v0.11.0...HEAD
92
+ [0.11.0]: https://github.com/boringcache/dagger_ruby/compare/v0.10.0...v0.11.0
93
+ [0.10.0]: https://github.com/boringcache/dagger_ruby/compare/v0.9.0...v0.10.0
68
94
  [0.9.0]: https://github.com/boringcache/dagger_ruby/compare/v0.8.0...v0.9.0
69
95
  [0.8.0]: https://github.com/boringcache/dagger_ruby/releases/tag/v0.8.0
70
96
  [0.6.0]: https://github.com/boringcache/dagger_ruby/releases/tag/v0.6.0
data/README.md CHANGED
@@ -12,7 +12,7 @@ covers a practical subset of the API instead of Dagger module code generation.
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Ruby 4.0 or newer
15
+ - Ruby 3.2 or newer
16
16
  - Dagger 0.21.8
17
17
  - Docker, Colima, or another container runtime supported by Dagger
18
18
 
@@ -131,18 +131,32 @@ available options.
131
131
  ```ruby
132
132
  config = DaggerRuby::Config.new(
133
133
  timeout: 300,
134
- progress: "plain",
134
+ progress: :pretty,
135
135
  log_output: $stderr,
136
+ runtime: :apple,
136
137
  )
137
138
 
138
139
  DaggerRuby.connection(config) do |client|
139
- # Build pipeline
140
+ progress = config.progress_reporter
141
+ container = client.container.from("alpine:3.22").with_exec(["apk", "add", "git"])
142
+ progress.step("[build] RUN apk add git") { container.sync }
140
143
  end
141
144
  ```
142
145
 
146
+ Use `progress: :pretty` for numbered, coloured application steps with live container output.
147
+ Use `:logs` for Dagger's unadorned streaming logs or `:plain` for the complete execution graph.
148
+ The supported modes are `:pretty`, `:auto`, `:plain`, `:tty`, `:dots`, and `:logs`.
149
+
143
150
  `DAGGER_QUIET`, `DAGGER_SILENT`, and `DAGGER_PROGRESS` can provide the same CLI
144
151
  preferences through the environment.
145
152
 
153
+ The client verifies the installed Dagger CLI before it starts a runner and then
154
+ verifies the connected engine version. This matters for runtime drivers because
155
+ the compatible CLI provisions the runner; merely pointing an older CLI at a
156
+ newer engine image is not sufficient. Use `runtime: :apple` or `runtime: :docker`
157
+ when more than one runtime is installed and automatic detection would be
158
+ ambiguous. A custom runner can be selected with `runner_host:`.
159
+
146
160
  ## Errors
147
161
 
148
162
  Connection, transport, GraphQL, and invalid-query failures use dedicated error
@@ -158,7 +172,7 @@ end
158
172
 
159
173
  ## Compatibility
160
174
 
161
- The current branch is tested against Ruby 4.0 and Dagger 0.21.8. Dagger's
175
+ The current branch is tested against maintained Ruby releases and Dagger 0.21.8. Dagger's
162
176
  GraphQL schema can change between releases, so each supported engine update is
163
177
  validated by the live integration suite before release. Earlier Ruby and Dagger
164
178
  versions are not covered by the current compatibility contract.
data/dagger_ruby.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  "services, caches, secrets, and filesystem operations with the Dagger engine."
14
14
  spec.homepage = "https://github.com/boringcache/dagger_ruby"
15
15
  spec.license = "MIT"
16
- spec.required_ruby_version = ">= 4.0.0"
16
+ spec.required_ruby_version = ">= 3.2.0"
17
17
 
18
18
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
19
 
@@ -1,8 +1,14 @@
1
1
  require "logger"
2
+ require_relative "version"
2
3
 
3
4
  module DaggerRuby
4
5
  class Config
5
- attr_reader :log_output, :workdir, :timeout, :quiet, :silent, :progress
6
+ ENGINE_IMAGE = "registry.dagger.io/engine".freeze
7
+ RUNTIMES = %i[auto apple docker].freeze
8
+ PROGRESS_MODES = %w[auto pretty plain tty dots logs].freeze
9
+
10
+ attr_reader :log_output, :workdir, :timeout, :quiet, :silent, :progress,
11
+ :runtime, :runner_host, :dagger_version, :verify_version
6
12
 
7
13
  def initialize(options = {})
8
14
  @log_output = options[:log_output]
@@ -10,7 +16,64 @@ module DaggerRuby
10
16
  @timeout = options[:timeout] || 600
11
17
  @quiet = options[:quiet] || ENV["DAGGER_QUIET"]&.to_i
12
18
  @silent = options[:silent] || ENV["DAGGER_SILENT"] == "true"
13
- @progress = options[:progress] || ENV.fetch("DAGGER_PROGRESS", nil)
19
+ @progress = normalize_progress(options[:progress] || ENV.fetch("DAGGER_PROGRESS", nil))
20
+ @runtime = normalize_runtime(options.fetch(:runtime, ENV.fetch("DAGGER_RUBY_RUNTIME", "auto")))
21
+ @runner_host = options[:runner_host]
22
+ @dagger_version = options.fetch(:dagger_version, DAGGER_VERSION).to_s.delete_prefix("v")
23
+ @verify_version = options.fetch(:verify_version, true)
24
+
25
+ raise ArgumentError, "runner_host and runtime cannot both be configured" if @runner_host && @runtime != :auto
26
+ end
27
+
28
+ def environment
29
+ environment = {}
30
+ environment["_EXPERIMENTAL_DAGGER_RUNNER_HOST"] = resolved_runner_host if resolved_runner_host
31
+ environment
32
+ end
33
+
34
+ def expected_engine_version
35
+ "v#{dagger_version}"
36
+ end
37
+
38
+ def pretty_progress? = progress == "pretty"
39
+
40
+ def streaming_logs? = %w[pretty logs].include?(progress)
41
+
42
+ def dagger_progress
43
+ pretty_progress? ? "logs" : progress
44
+ end
45
+
46
+ def progress_reporter(out: $stdout, enabled: true, environment: ENV)
47
+ enabled &&= pretty_progress?
48
+ color = enabled && out.respond_to?(:tty?) && out.tty? && !environment.key?("NO_COLOR") &&
49
+ environment.fetch("TERM", "") != "dumb"
50
+ Progress.new(out: out, enabled: enabled, streaming: streaming_logs?, color: color,
51
+ relay: enabled && Progress::Relay.from_environment(environment))
52
+ end
53
+
54
+ private
55
+
56
+ def normalize_runtime(value)
57
+ runtime = value.to_s.downcase.to_sym
58
+ return runtime if RUNTIMES.include?(runtime)
59
+
60
+ raise ArgumentError, "Unknown Dagger runtime '#{value}'. Use auto, apple, or docker."
61
+ end
62
+
63
+ def normalize_progress(value)
64
+ return unless value
65
+
66
+ progress = value.to_s.downcase
67
+ return progress if PROGRESS_MODES.include?(progress)
68
+
69
+ raise ArgumentError, "Unknown Dagger progress '#{value}'. Use pretty, auto, plain, tty, dots, or logs."
70
+ end
71
+
72
+ def resolved_runner_host
73
+ return runner_host if runner_host
74
+ return if runtime == :auto
75
+
76
+ "image+#{runtime}://#{ENGINE_IMAGE}:#{expected_engine_version}"
14
77
  end
15
78
  end
16
79
  end
@@ -98,6 +98,26 @@ module DaggerRuby
98
98
  chain_operation("withEntrypoint", entrypoint_args)
99
99
  end
100
100
 
101
+ def with_default_args(args)
102
+ chain_operation("withDefaultArgs", { "args" => args })
103
+ end
104
+
105
+ def with_docker_healthcheck(args, opts = {})
106
+ healthcheck_args = { "args" => args }
107
+ healthcheck_args["shell"] = opts[:shell] if opts.key?(:shell)
108
+ healthcheck_args["interval"] = opts[:interval] if opts[:interval]
109
+ healthcheck_args["timeout"] = opts[:timeout] if opts[:timeout]
110
+ healthcheck_args["startPeriod"] = opts[:start_period] if opts[:start_period]
111
+ healthcheck_args["startInterval"] = opts[:start_interval] if opts[:start_interval]
112
+ healthcheck_args["retries"] = opts[:retries] if opts[:retries]
113
+
114
+ chain_operation("withDockerHealthcheck", healthcheck_args)
115
+ end
116
+
117
+ def without_docker_healthcheck
118
+ chain_operation("withoutDockerHealthcheck")
119
+ end
120
+
101
121
  def with_user(name)
102
122
  chain_operation("withUser", { "name" => name })
103
123
  end
@@ -259,6 +279,17 @@ module DaggerRuby
259
279
  get_scalar("entrypoint")
260
280
  end
261
281
 
282
+ def default_args
283
+ get_scalar("defaultArgs")
284
+ end
285
+
286
+ def docker_healthcheck
287
+ get_selection(
288
+ "dockerHealthcheck",
289
+ "args shell interval timeout startPeriod startInterval retries",
290
+ )
291
+ end
292
+
262
293
  def env_variables
263
294
  get_selection("envVariables", "name value")
264
295
  end
@@ -305,6 +336,16 @@ module DaggerRuby
305
336
  export(path, opts)
306
337
  end
307
338
 
339
+ def export_image(name, opts = {})
340
+ args = { "name" => name }
341
+ args["platformVariants"] = graphql_ids(opts[:platform_variants]) if opts[:platform_variants]
342
+ args["forcedCompression"] = QueryBuilder.enum_value(opts[:forced_compression]) if opts[:forced_compression]
343
+ args["mediaTypes"] = QueryBuilder.enum_value(opts[:media_types]) if opts[:media_types]
344
+
345
+ get_scalar("exportImage", args)
346
+ name
347
+ end
348
+
308
349
  def publish(address, opts = {})
309
350
  args = { "address" => address }
310
351
  args["platformVariants"] = graphql_ids(opts[:platform_variants]) if opts[:platform_variants]
@@ -6,4 +6,5 @@ module DaggerRuby
6
6
  class HTTPError < DaggerError; end
7
7
  class InvalidQueryError < DaggerError; end
8
8
  class ConnectionError < DaggerError; end
9
+ class VersionMismatchError < ConnectionError; end
9
10
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "socket"
5
+
6
+ module DaggerRuby
7
+ class Progress
8
+ SOCKET_ENV = "DAGGER_RUBY_PROGRESS_SOCKET"
9
+
10
+ class Relay
11
+ def self.from_environment(environment)
12
+ path = environment[SOCKET_ENV]
13
+ new(path) if path
14
+ end
15
+
16
+ def initialize(path)
17
+ @socket = UNIXSocket.new(path)
18
+ end
19
+
20
+ def emit(event)
21
+ @socket.puts(JSON.generate(event))
22
+ @socket.flush
23
+ raise IOError, "Dagger Ruby progress relay disconnected" unless @socket.gets
24
+ end
25
+
26
+ def close
27
+ @socket.close unless @socket.closed?
28
+ end
29
+ end
30
+
31
+ def self.silent
32
+ @silent ||= new(enabled: false)
33
+ end
34
+
35
+ def initialize(out: $stdout, enabled: true, streaming: false, color: false, relay: nil)
36
+ @out = out
37
+ @enabled = enabled
38
+ @streaming = streaming
39
+ @color = color
40
+ @relay = relay
41
+ @step = 0
42
+ end
43
+
44
+ def streaming? = @streaming
45
+
46
+ def step(name)
47
+ return yield unless @enabled
48
+
49
+ number = next_step
50
+ started_at = monotonic_time
51
+ start_step(number, name)
52
+ result = yield
53
+ finish_step(number, "DONE", elapsed_since(started_at))
54
+ result
55
+ rescue Interrupt
56
+ finish_step(number, "CANCELED", elapsed_since(started_at)) if number
57
+ raise
58
+ rescue StandardError
59
+ finish_step(number, "ERROR", elapsed_since(started_at)) if number
60
+ raise
61
+ end
62
+
63
+ def write(output = nil)
64
+ return unless @enabled
65
+
66
+ output = yield if block_given?
67
+ text = output.to_s
68
+ text.each_line { |line| @out.print " #{line}" }
69
+ @out.puts unless text.empty? || text.end_with?("\n")
70
+ end
71
+
72
+ def message(text)
73
+ return unless @enabled
74
+ return @relay.emit(type: "message", text: text.to_s) if @relay
75
+
76
+ @out.puts text
77
+ @out.flush
78
+ end
79
+
80
+ def close
81
+ @relay&.close
82
+ end
83
+
84
+ def render(event)
85
+ case event.fetch("type")
86
+ when "message"
87
+ @out.puts event.fetch("text")
88
+ when "start"
89
+ @out.puts step_heading(event.fetch("step"), event.fetch("name"))
90
+ when "finish"
91
+ status = event.fetch("status")
92
+ @out.puts step_result(event.fetch("step"), status, event.fetch("elapsed"), color: status_color(status))
93
+ end
94
+ @out.flush
95
+ end
96
+
97
+ private
98
+
99
+ def next_step
100
+ @step += 1
101
+ end
102
+
103
+ def start_step(number, name)
104
+ return @relay.emit(type: "start", step: number, name: name) if @relay
105
+
106
+ @out.puts step_heading(number, name)
107
+ end
108
+
109
+ def finish_step(number, status, elapsed)
110
+ return @relay.emit(type: "finish", step: number, status: status, elapsed: elapsed) if @relay
111
+
112
+ @out.puts step_result(number, status, elapsed, color: status_color(status))
113
+ end
114
+
115
+ def status_color(status)
116
+ { "DONE" => 32, "CANCELED" => 33 }.fetch(status, 31)
117
+ end
118
+
119
+ def monotonic_time
120
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
121
+ end
122
+
123
+ def elapsed_since(started_at)
124
+ format("%.1fs", monotonic_time - started_at)
125
+ end
126
+
127
+ def step_heading(number, name)
128
+ return "##{number} #{name}" unless @color
129
+
130
+ highlighted = name.sub(/\A(\[[^\]]+\])/) { paint(_1, 36) }
131
+ .sub(/\b(FROM|RUN|COPY)\b/) { paint(_1, 1) }
132
+ "#{step_number(number)} #{highlighted}"
133
+ end
134
+
135
+ def step_result(number, status, elapsed, color:)
136
+ return "##{number} #{status} #{elapsed}" unless @color
137
+
138
+ "#{step_number(number)} #{paint(status, "1;#{color}")} #{elapsed}"
139
+ end
140
+
141
+ def step_number(number)
142
+ @color ? paint("##{number}", "1;34") : "##{number}"
143
+ end
144
+
145
+ def paint(text, code)
146
+ "\e[#{code}m#{text}\e[0m"
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module DaggerRuby
6
+ class ProgressRunner
7
+ DAGGER_EXEC_HEADING = /\A(?:\e\[[0-9;]*m)*▼ withExec\b/
8
+ INTERRUPT_TIMEOUT = 1
9
+ TERMINATION_TIMEOUT = 1
10
+
11
+ def initialize(out: $stderr, result_out: $stdout, environment: ENV)
12
+ @out = out
13
+ @result_out = result_out
14
+ @environment = environment
15
+ @started = false
16
+ @started_mutex = Mutex.new
17
+ end
18
+
19
+ def run(command, environment: {})
20
+ reset_progress_state!
21
+ Dir.mktmpdir("drp-", "/tmp") do |directory|
22
+ server = UNIXServer.new(::File.join(directory, "progress.sock"))
23
+ listener = listen(server)
24
+ primary_output, primary_writer = IO.pipe
25
+ primary_reader = relay_primary_output(primary_output)
26
+ dagger_output, dagger_writer = IO.pipe
27
+ output_reader = relay_dagger_output(dagger_output)
28
+ pid = Process.spawn(
29
+ environment.merge(Progress::SOCKET_ENV => server.path),
30
+ *command,
31
+ in: ::File::NULL,
32
+ out: primary_writer,
33
+ err: dagger_writer,
34
+ pgroup: true,
35
+ )
36
+ primary_writer.close
37
+ dagger_writer.close
38
+ _, status = Process.wait2(pid)
39
+ clean_process_group(pid)
40
+ server.close
41
+ listener.join
42
+ primary_reader.join
43
+ output_reader.join
44
+ exit_status(status)
45
+ rescue Interrupt
46
+ terminate(pid)
47
+ @out.puts "\nBuild canceled."
48
+ @out.flush
49
+ 130
50
+ ensure
51
+ close_io(primary_output)
52
+ close_io(primary_writer)
53
+ close_io(dagger_output)
54
+ close_io(dagger_writer)
55
+ close_io(server)
56
+ stop_thread(listener)
57
+ stop_thread(primary_reader)
58
+ stop_thread(output_reader)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def listen(server)
65
+ Thread.new do
66
+ socket = server.accept
67
+ renderer = Progress.new(out: @out, color: color?)
68
+ socket.each_line do |line|
69
+ renderer.render(JSON.parse(line))
70
+ progress_started!
71
+ socket.puts("ok")
72
+ socket.flush
73
+ end
74
+ rescue IOError, Errno::EBADF, Errno::EPIPE
75
+ nil
76
+ ensure
77
+ socket&.close
78
+ end
79
+ end
80
+
81
+ def relay_dagger_output(input)
82
+ Thread.new do
83
+ buffered = []
84
+ input.each_line do |line|
85
+ if progress_started?
86
+ @out.write(line) unless line.match?(DAGGER_EXEC_HEADING)
87
+ @out.flush
88
+ else
89
+ buffered << line
90
+ end
91
+ end
92
+ @out.write(buffered.join) unless progress_started?
93
+ @out.flush
94
+ rescue IOError, Errno::EBADF
95
+ nil
96
+ ensure
97
+ input.close unless input.closed?
98
+ end
99
+ end
100
+
101
+ def relay_primary_output(input)
102
+ Thread.new do
103
+ loop do
104
+ @result_out.write(input.readpartial(4096))
105
+ @result_out.flush
106
+ end
107
+ rescue IOError, Errno::EBADF
108
+ nil
109
+ ensure
110
+ input.close unless input.closed?
111
+ end
112
+ end
113
+
114
+ def progress_started!
115
+ @started_mutex.synchronize { @started = true }
116
+ end
117
+
118
+ def reset_progress_state!
119
+ @started_mutex.synchronize { @started = false }
120
+ end
121
+
122
+ def progress_started?
123
+ @started_mutex.synchronize { @started }
124
+ end
125
+
126
+ def close_io(io)
127
+ io.close if io && !io.closed?
128
+ end
129
+
130
+ def stop_thread(thread)
131
+ return unless thread
132
+
133
+ thread.kill if thread.alive?
134
+ thread.join
135
+ end
136
+
137
+ def terminate(pid)
138
+ return unless pid
139
+
140
+ reaped = false
141
+ begin
142
+ signal_process_group("INT", pid)
143
+ reaped = wait_for_exit(pid, timeout: INTERRUPT_TIMEOUT)
144
+ if process_group_alive?(pid)
145
+ signal_process_group("TERM", pid)
146
+ reaped ||= wait_for_exit(pid, timeout: TERMINATION_TIMEOUT)
147
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT) if reaped
148
+ end
149
+ signal_process_group("KILL", pid) if process_group_alive?(pid)
150
+ ensure
151
+ reap(pid) unless reaped
152
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
153
+ end
154
+ end
155
+
156
+ def clean_process_group(pid)
157
+ return unless process_group_alive?(pid)
158
+
159
+ signal_process_group("TERM", pid)
160
+ return if wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
161
+
162
+ signal_process_group("KILL", pid)
163
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
164
+ end
165
+
166
+ def process_group_alive?(pid)
167
+ Process.kill(0, -pid)
168
+ true
169
+ rescue Errno::ESRCH
170
+ false
171
+ end
172
+
173
+ def wait_for_process_group(pid, timeout:)
174
+ deadline = monotonic_time + timeout
175
+ loop do
176
+ return true unless process_group_alive?(pid)
177
+ return false if monotonic_time >= deadline
178
+
179
+ sleep 0.05
180
+ end
181
+ end
182
+
183
+ def signal_process_group(signal, pid)
184
+ Process.kill(signal, -pid)
185
+ rescue Errno::ESRCH
186
+ nil
187
+ end
188
+
189
+ def wait_for_exit(pid, timeout:)
190
+ deadline = monotonic_time + timeout
191
+ loop do
192
+ return true if Process.waitpid(pid, Process::WNOHANG)
193
+ return false if monotonic_time >= deadline
194
+
195
+ sleep 0.05
196
+ end
197
+ rescue Errno::ECHILD
198
+ true
199
+ end
200
+
201
+ def reap(pid)
202
+ Process.wait(pid)
203
+ rescue Errno::ECHILD
204
+ nil
205
+ end
206
+
207
+ def monotonic_time
208
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
209
+ end
210
+
211
+ def exit_status(status)
212
+ status.exitstatus || (128 + status.termsig)
213
+ end
214
+
215
+ def color?
216
+ @out.respond_to?(:tty?) && @out.tty? && !@environment.key?("NO_COLOR") &&
217
+ @environment.fetch("TERM", "") != "dumb"
218
+ end
219
+ end
220
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DaggerRuby
4
- VERSION = "0.9.0"
4
+ VERSION = "0.11.0"
5
5
  DAGGER_VERSION = "0.21.8"
6
6
  end
data/lib/dagger_ruby.rb CHANGED
@@ -1,16 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dagger_ruby/version"
4
+ require_relative "dagger_ruby/progress"
5
+ require_relative "dagger_ruby/progress_runner"
4
6
  require_relative "dagger_ruby/client"
5
7
  require_relative "dagger_ruby/config"
6
8
  require_relative "dagger_ruby/errors"
9
+ require "open3"
7
10
 
8
11
  module DaggerRuby
9
12
  class << self
10
13
  def connection(config = nil, &)
14
+ config ||= Config.new
11
15
  return connection_in_current_session(config, &) if dagger_session?
12
16
 
13
- exec(*dagger_run_command(config))
17
+ verify_cli_version!(config) if config.verify_version
18
+ if config.pretty_progress?
19
+ exit ProgressRunner.new.run(dagger_run_command(config), environment: config.environment)
20
+ end
21
+ exec(config.environment, *dagger_run_command(config))
22
+ rescue Interrupt
23
+ exit 130 if config.pretty_progress?
24
+
25
+ raise
14
26
  end
15
27
 
16
28
  private
@@ -21,6 +33,12 @@ module DaggerRuby
21
33
 
22
34
  def connection_in_current_session(config)
23
35
  client = Client.new(config: config)
36
+ begin
37
+ verify_engine_version!(client, config) if config.verify_version
38
+ rescue StandardError
39
+ client.close
40
+ raise
41
+ end
24
42
  return client unless block_given?
25
43
 
26
44
  begin
@@ -30,6 +48,14 @@ module DaggerRuby
30
48
  end
31
49
  end
32
50
 
51
+ def verify_engine_version!(client, config)
52
+ actual = client.execute_query("query { version }").fetch("version")
53
+ return if actual == config.expected_engine_version
54
+
55
+ raise VersionMismatchError,
56
+ "Dagger engine #{actual} is incompatible with this client; expected #{config.expected_engine_version}"
57
+ end
58
+
33
59
  def dagger_run_command(config)
34
60
  [
35
61
  "dagger",
@@ -44,6 +70,19 @@ module DaggerRuby
44
70
  ]
45
71
  end
46
72
 
73
+ def verify_cli_version!(config)
74
+ stdout, stderr, status = Open3.capture3("dagger", "version")
75
+ output = [stdout, stderr].join(" ").strip
76
+ actual = output[/\bv\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/]
77
+ return if status.success? && actual == config.expected_engine_version
78
+
79
+ found = actual || (output.empty? ? "an unreadable version" : output)
80
+ raise VersionMismatchError,
81
+ "Dagger CLI #{found} is incompatible with this client; install #{config.expected_engine_version}"
82
+ rescue Errno::ENOENT
83
+ raise VersionMismatchError, "Dagger CLI is not installed; install #{config.expected_engine_version}"
84
+ end
85
+
47
86
  def quiet_flags(config)
48
87
  quiet = config&.quiet || ENV["DAGGER_QUIET"]&.to_i
49
88
  quiet&.positive? ? ["-q"] * quiet : []
@@ -54,7 +93,7 @@ module DaggerRuby
54
93
  end
55
94
 
56
95
  def progress_option(config)
57
- progress = config&.progress || ENV.fetch("DAGGER_PROGRESS", nil)
96
+ progress = config&.dagger_progress || ENV.fetch("DAGGER_PROGRESS", nil)
58
97
  progress ? ["--progress", progress] : []
59
98
  end
60
99
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dagger_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -75,6 +75,8 @@ files:
75
75
  - lib/dagger_ruby/file.rb
76
76
  - lib/dagger_ruby/git_repository.rb
77
77
  - lib/dagger_ruby/host.rb
78
+ - lib/dagger_ruby/progress.rb
79
+ - lib/dagger_ruby/progress_runner.rb
78
80
  - lib/dagger_ruby/query_builder.rb
79
81
  - lib/dagger_ruby/secret.rb
80
82
  - lib/dagger_ruby/service.rb
@@ -85,7 +87,7 @@ licenses:
85
87
  metadata:
86
88
  allowed_push_host: https://rubygems.org
87
89
  homepage_uri: https://github.com/boringcache/dagger_ruby
88
- source_code_uri: https://github.com/boringcache/dagger_ruby/tree/v0.9.0
90
+ source_code_uri: https://github.com/boringcache/dagger_ruby/tree/v0.11.0
89
91
  changelog_uri: https://github.com/boringcache/dagger_ruby/blob/main/CHANGELOG.md
90
92
  bug_tracker_uri: https://github.com/boringcache/dagger_ruby/issues
91
93
  documentation_uri: https://github.com/boringcache/dagger_ruby#readme
@@ -98,7 +100,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
98
100
  requirements:
99
101
  - - ">="
100
102
  - !ruby/object:Gem::Version
101
- version: 4.0.0
103
+ version: 3.2.0
102
104
  required_rubygems_version: !ruby/object:Gem::Requirement
103
105
  requirements:
104
106
  - - ">="