dagger_ruby 0.10.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: 4a4bcab9db019b25e782aab84ab1e3b154bda15f64cb7d1ffdd522d14cbfb729
4
- data.tar.gz: 41e902336fe58545784c21e85a17cc55961a9aab6ad2c6370547ffb4d4779eb5
3
+ metadata.gz: ebaf58b886e0c88802e55c2abb2f10676c4b00466dfd24fc69d6f38ead563e6a
4
+ data.tar.gz: ad9abb4ee776703af437e713bf3d61a99b80e0f0d9586dadda2a98c689faedf7
5
5
  SHA512:
6
- metadata.gz: 897cdcb244f34e704a70f2bb47fdeadeb946c2c29e0fff763cf0b487122657a11d2bee3d664593c0b268baf45eba0cbf9b8da1e889eb601e1f4ad6ed7056e1b2
7
- data.tar.gz: c52a3cfff5a10a1ef6c8bc1e522c56bf423642dbfbc57bcc76c03e84a61e31e8f346a8157bffb53e112d2a77cdfd044b6d4c562784f9afa785f0460d02f98864
6
+ metadata.gz: ac82debf94e095271f87339a9af2f82bc10fc3a07f049a343073c35effdb018c8d69e58cab7d446239303b4a0647e31cb69ea53e4f2193bf77c3bb53c3b676b4
7
+ data.tar.gz: ba23033d17a9803d92ba629d1cf4db2ae86b788081a54017cb4443a6122ab69c763f968bb7539429864771e720484a1c85008de5505da48575dcdff23ee76f53
data/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ 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
+
9
20
  ## [0.10.0] - 2026-08-25
10
21
 
11
22
  ### Added
@@ -77,7 +88,8 @@ All notable changes are recorded here. The format follows
77
88
 
78
89
  - Initial container, directory, file, cache, secret, Git, and GraphQL client.
79
90
 
80
- [Unreleased]: https://github.com/boringcache/dagger_ruby/compare/v0.10.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
81
93
  [0.10.0]: https://github.com/boringcache/dagger_ruby/compare/v0.9.0...v0.10.0
82
94
  [0.9.0]: https://github.com/boringcache/dagger_ruby/compare/v0.8.0...v0.9.0
83
95
  [0.8.0]: https://github.com/boringcache/dagger_ruby/releases/tag/v0.8.0
data/README.md CHANGED
@@ -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
 
@@ -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,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.10.0"
4
+ VERSION = "0.11.0"
5
5
  DAGGER_VERSION = "0.21.8"
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.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.10.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