dagger_ruby 0.10.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a4bcab9db019b25e782aab84ab1e3b154bda15f64cb7d1ffdd522d14cbfb729
4
- data.tar.gz: 41e902336fe58545784c21e85a17cc55961a9aab6ad2c6370547ffb4d4779eb5
3
+ metadata.gz: 61be4b7342e4586fce56e0802de200a7231c85e8f2781ff94b7b8d987f265db3
4
+ data.tar.gz: d2cde21a762e08d63e345e6c1c1bd5890fc2e48e1b60f65eb8782c003f9ce2a6
5
5
  SHA512:
6
- metadata.gz: 897cdcb244f34e704a70f2bb47fdeadeb946c2c29e0fff763cf0b487122657a11d2bee3d664593c0b268baf45eba0cbf9b8da1e889eb601e1f4ad6ed7056e1b2
7
- data.tar.gz: c52a3cfff5a10a1ef6c8bc1e522c56bf423642dbfbc57bcc76c03e84a61e31e8f346a8157bffb53e112d2a77cdfd044b6d4c562784f9afa785f0460d02f98864
6
+ metadata.gz: d10eff81b4a146a80bf6945e163318b78b59e0ed6cabe4052652af84795b6ae5ccce0c75f4d385258a7275160d9ea36d13ecddf706a1b3a33ad8a135a0ec489c
7
+ data.tar.gz: 999a2aa2e9e994425f77f8830fd621493d67f6ad6f37a0984b6c5065ab155b8cb4b82feb2aac968b9636e3996ee6c2f0c33ebdbee7e3ed26d9d442a5a81d64ea
data/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ All notable changes are recorded here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.1] - 2026-10-02
10
+
11
+ ### Changed
12
+
13
+ - Update development dependencies.
14
+ - Update the supported Dagger CLI and engine from 0.21.8 to 0.21.9.
15
+
16
+ ### Fixed
17
+
18
+ - Show an engine startup status in pretty progress mode when Dagger takes more than one second to start.
19
+
20
+ ## [0.11.0] - 2026-08-27
21
+
22
+ ### Added
23
+
24
+ - First-class `progress: :pretty` support for Docker-style application steps with live container output.
25
+ - Support for Dagger's `logs` and `dots` progress frontends.
26
+
27
+ ### Fixed
28
+
29
+ - Keep engine startup failures visible and reap the complete Dagger process group after completion or interruption.
30
+
9
31
  ## [0.10.0] - 2026-08-25
10
32
 
11
33
  ### Added
@@ -77,7 +99,8 @@ All notable changes are recorded here. The format follows
77
99
 
78
100
  - Initial container, directory, file, cache, secret, Git, and GraphQL client.
79
101
 
80
- [Unreleased]: https://github.com/boringcache/dagger_ruby/compare/v0.10.0...HEAD
102
+ [Unreleased]: https://github.com/boringcache/dagger_ruby/compare/v0.11.0...HEAD
103
+ [0.11.0]: https://github.com/boringcache/dagger_ruby/compare/v0.10.0...v0.11.0
81
104
  [0.10.0]: https://github.com/boringcache/dagger_ruby/compare/v0.9.0...v0.10.0
82
105
  [0.9.0]: https://github.com/boringcache/dagger_ruby/compare/v0.8.0...v0.9.0
83
106
  [0.8.0]: https://github.com/boringcache/dagger_ruby/releases/tag/v0.8.0
data/README.md CHANGED
@@ -13,7 +13,7 @@ covers a practical subset of the API instead of Dagger module code generation.
13
13
  ## Requirements
14
14
 
15
15
  - Ruby 3.2 or newer
16
- - Dagger 0.21.8
16
+ - Dagger 0.21.9
17
17
  - Docker, Colima, or another container runtime supported by Dagger
18
18
 
19
19
  ## Installation
@@ -131,16 +131,22 @@ 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
136
  runtime: :apple,
137
137
  )
138
138
 
139
139
  DaggerRuby.connection(config) do |client|
140
- # 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 }
141
143
  end
142
144
  ```
143
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
+
144
150
  `DAGGER_QUIET`, `DAGGER_SILENT`, and `DAGGER_PROGRESS` can provide the same CLI
145
151
  preferences through the environment.
146
152
 
@@ -166,7 +172,7 @@ end
166
172
 
167
173
  ## Compatibility
168
174
 
169
- The current branch is tested against maintained Ruby releases and Dagger 0.21.8. Dagger's
175
+ The current branch is tested against maintained Ruby releases and Dagger 0.21.9. Dagger's
170
176
  GraphQL schema can change between releases, so each supported engine update is
171
177
  validated by the live integration suite before release. Earlier Ruby and Dagger
172
178
  versions are not covered by the current compatibility contract.
@@ -5,6 +5,7 @@ module DaggerRuby
5
5
  class Config
6
6
  ENGINE_IMAGE = "registry.dagger.io/engine".freeze
7
7
  RUNTIMES = %i[auto apple docker].freeze
8
+ PROGRESS_MODES = %w[auto pretty plain tty dots logs].freeze
8
9
 
9
10
  attr_reader :log_output, :workdir, :timeout, :quiet, :silent, :progress,
10
11
  :runtime, :runner_host, :dagger_version, :verify_version
@@ -15,7 +16,7 @@ module DaggerRuby
15
16
  @timeout = options[:timeout] || 600
16
17
  @quiet = options[:quiet] || ENV["DAGGER_QUIET"]&.to_i
17
18
  @silent = options[:silent] || ENV["DAGGER_SILENT"] == "true"
18
- @progress = options[:progress] || ENV.fetch("DAGGER_PROGRESS", nil)
19
+ @progress = normalize_progress(options[:progress] || ENV.fetch("DAGGER_PROGRESS", nil))
19
20
  @runtime = normalize_runtime(options.fetch(:runtime, ENV.fetch("DAGGER_RUBY_RUNTIME", "auto")))
20
21
  @runner_host = options[:runner_host]
21
22
  @dagger_version = options.fetch(:dagger_version, DAGGER_VERSION).to_s.delete_prefix("v")
@@ -34,6 +35,22 @@ module DaggerRuby
34
35
  "v#{dagger_version}"
35
36
  end
36
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
+
37
54
  private
38
55
 
39
56
  def normalize_runtime(value)
@@ -43,6 +60,15 @@ module DaggerRuby
43
60
  raise ArgumentError, "Unknown Dagger runtime '#{value}'. Use auto, apple, or docker."
44
61
  end
45
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
+
46
72
  def resolved_runner_host
47
73
  return runner_host if runner_host
48
74
  return if runtime == :auto
@@ -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,241 @@
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
+ STARTUP_NOTICE_DELAY = 1
10
+ TERMINATION_TIMEOUT = 1
11
+
12
+ def initialize(out: $stderr, result_out: $stdout, environment: ENV)
13
+ @out = out
14
+ @result_out = result_out
15
+ @environment = environment
16
+ @started = false
17
+ @started_mutex = Mutex.new
18
+ end
19
+
20
+ def run(command, environment: {})
21
+ reset_progress_state!
22
+ Dir.mktmpdir("drp-", "/tmp") do |directory|
23
+ server = UNIXServer.new(::File.join(directory, "progress.sock"))
24
+ listener = listen(server)
25
+ primary_output, primary_writer = IO.pipe
26
+ primary_reader = relay_primary_output(primary_output)
27
+ dagger_output, dagger_writer = IO.pipe
28
+ output_reader = relay_dagger_output(dagger_output)
29
+ pid = Process.spawn(
30
+ environment.merge(Progress::SOCKET_ENV => server.path),
31
+ *command,
32
+ in: ::File::NULL,
33
+ out: primary_writer,
34
+ err: dagger_writer,
35
+ pgroup: true,
36
+ )
37
+ primary_writer.close
38
+ dagger_writer.close
39
+ startup_notice = report_slow_startup
40
+ _, status = Process.wait2(pid)
41
+ clean_process_group(pid)
42
+ server.close
43
+ listener.join
44
+ primary_reader.join
45
+ output_reader.join
46
+ exit_status(status)
47
+ rescue Interrupt
48
+ terminate(pid)
49
+ @out.puts "\nBuild canceled."
50
+ @out.flush
51
+ 130
52
+ ensure
53
+ close_io(primary_output)
54
+ close_io(primary_writer)
55
+ close_io(dagger_output)
56
+ close_io(dagger_writer)
57
+ close_io(server)
58
+ stop_thread(listener)
59
+ stop_thread(primary_reader)
60
+ stop_thread(output_reader)
61
+ stop_thread(startup_notice)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def listen(server)
68
+ Thread.new do
69
+ socket = server.accept
70
+ renderer = Progress.new(out: @out, color: color?)
71
+ socket.each_line do |line|
72
+ render_progress(renderer, JSON.parse(line))
73
+ socket.puts("ok")
74
+ socket.flush
75
+ end
76
+ rescue IOError, Errno::EBADF, Errno::EPIPE
77
+ nil
78
+ ensure
79
+ socket&.close
80
+ end
81
+ end
82
+
83
+ def report_slow_startup
84
+ Thread.new do
85
+ sleep startup_notice_delay
86
+ @started_mutex.synchronize do
87
+ next if @started
88
+
89
+ @out.puts "Dagger is still starting. It may be downloading the engine image."
90
+ @out.flush
91
+ end
92
+ end
93
+ end
94
+
95
+ def relay_dagger_output(input)
96
+ Thread.new do
97
+ buffered = []
98
+ input.each_line do |line|
99
+ if progress_started?
100
+ @out.write(line) unless line.match?(DAGGER_EXEC_HEADING)
101
+ @out.flush
102
+ else
103
+ buffered << line
104
+ end
105
+ end
106
+ @out.write(buffered.join) unless progress_started?
107
+ @out.flush
108
+ rescue IOError, Errno::EBADF
109
+ nil
110
+ ensure
111
+ input.close unless input.closed?
112
+ end
113
+ end
114
+
115
+ def relay_primary_output(input)
116
+ Thread.new do
117
+ loop do
118
+ @result_out.write(input.readpartial(4096))
119
+ @result_out.flush
120
+ end
121
+ rescue IOError, Errno::EBADF
122
+ nil
123
+ ensure
124
+ input.close unless input.closed?
125
+ end
126
+ end
127
+
128
+ def render_progress(renderer, event)
129
+ @started_mutex.synchronize do
130
+ @started = true
131
+ renderer.render(event)
132
+ end
133
+ end
134
+
135
+ def reset_progress_state!
136
+ @started_mutex.synchronize { @started = false }
137
+ end
138
+
139
+ def progress_started?
140
+ @started_mutex.synchronize { @started }
141
+ end
142
+
143
+ def close_io(io)
144
+ io.close if io && !io.closed?
145
+ end
146
+
147
+ def stop_thread(thread)
148
+ return unless thread
149
+
150
+ thread.kill if thread.alive?
151
+ thread.join
152
+ end
153
+
154
+ def terminate(pid)
155
+ return unless pid
156
+
157
+ reaped = false
158
+ begin
159
+ signal_process_group("INT", pid)
160
+ reaped = wait_for_exit(pid, timeout: INTERRUPT_TIMEOUT)
161
+ if process_group_alive?(pid)
162
+ signal_process_group("TERM", pid)
163
+ reaped ||= wait_for_exit(pid, timeout: TERMINATION_TIMEOUT)
164
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT) if reaped
165
+ end
166
+ signal_process_group("KILL", pid) if process_group_alive?(pid)
167
+ ensure
168
+ reap(pid) unless reaped
169
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
170
+ end
171
+ end
172
+
173
+ def clean_process_group(pid)
174
+ return unless process_group_alive?(pid)
175
+
176
+ signal_process_group("TERM", pid)
177
+ return if wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
178
+
179
+ signal_process_group("KILL", pid)
180
+ wait_for_process_group(pid, timeout: TERMINATION_TIMEOUT)
181
+ end
182
+
183
+ def process_group_alive?(pid)
184
+ Process.kill(0, -pid)
185
+ true
186
+ rescue Errno::ESRCH
187
+ false
188
+ end
189
+
190
+ def wait_for_process_group(pid, timeout:)
191
+ deadline = monotonic_time + timeout
192
+ loop do
193
+ return true unless process_group_alive?(pid)
194
+ return false if monotonic_time >= deadline
195
+
196
+ sleep 0.05
197
+ end
198
+ end
199
+
200
+ def signal_process_group(signal, pid)
201
+ Process.kill(signal, -pid)
202
+ rescue Errno::ESRCH
203
+ nil
204
+ end
205
+
206
+ def wait_for_exit(pid, timeout:)
207
+ deadline = monotonic_time + timeout
208
+ loop do
209
+ return true if Process.waitpid(pid, Process::WNOHANG)
210
+ return false if monotonic_time >= deadline
211
+
212
+ sleep 0.05
213
+ end
214
+ rescue Errno::ECHILD
215
+ true
216
+ end
217
+
218
+ def reap(pid)
219
+ Process.wait(pid)
220
+ rescue Errno::ECHILD
221
+ nil
222
+ end
223
+
224
+ def monotonic_time
225
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
226
+ end
227
+
228
+ def startup_notice_delay
229
+ STARTUP_NOTICE_DELAY
230
+ end
231
+
232
+ def exit_status(status)
233
+ status.exitstatus || (128 + status.termsig)
234
+ end
235
+
236
+ def color?
237
+ @out.respond_to?(:tty?) && @out.tty? && !@environment.key?("NO_COLOR") &&
238
+ @environment.fetch("TERM", "") != "dumb"
239
+ end
240
+ end
241
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DaggerRuby
4
- VERSION = "0.10.0"
5
- DAGGER_VERSION = "0.21.8"
4
+ VERSION = "0.11.1"
5
+ DAGGER_VERSION = "0.21.9"
6
6
  end
data/lib/dagger_ruby.rb CHANGED
@@ -1,6 +1,8 @@
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"
@@ -13,7 +15,14 @@ module DaggerRuby
13
15
  return connection_in_current_session(config, &) if dagger_session?
14
16
 
15
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
16
21
  exec(config.environment, *dagger_run_command(config))
22
+ rescue Interrupt
23
+ exit 130 if config.pretty_progress?
24
+
25
+ raise
17
26
  end
18
27
 
19
28
  private
@@ -84,7 +93,7 @@ module DaggerRuby
84
93
  end
85
94
 
86
95
  def progress_option(config)
87
- progress = config&.progress || ENV.fetch("DAGGER_PROGRESS", nil)
96
+ progress = config&.dagger_progress || ENV.fetch("DAGGER_PROGRESS", nil)
88
97
  progress ? ["--progress", progress] : []
89
98
  end
90
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.10.0
4
+ version: 0.11.1
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.10.0
90
+ source_code_uri: https://github.com/boringcache/dagger_ruby/tree/v0.11.1
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