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 +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +18 -4
- data/dagger_ruby.gemspec +1 -1
- data/lib/dagger_ruby/config.rb +65 -2
- data/lib/dagger_ruby/container.rb +41 -0
- data/lib/dagger_ruby/errors.rb +1 -0
- data/lib/dagger_ruby/progress.rb +149 -0
- data/lib/dagger_ruby/progress_runner.rb +220 -0
- data/lib/dagger_ruby/version.rb +1 -1
- data/lib/dagger_ruby.rb +41 -2
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebaf58b886e0c88802e55c2abb2f10676c4b00466dfd24fc69d6f38ead563e6a
|
|
4
|
+
data.tar.gz: ad9abb4ee776703af437e713bf3d61a99b80e0f0d9586dadda2a98c689faedf7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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:
|
|
134
|
+
progress: :pretty,
|
|
135
135
|
log_output: $stderr,
|
|
136
|
+
runtime: :apple,
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
DaggerRuby.connection(config) do |client|
|
|
139
|
-
|
|
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
|
|
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 = ">=
|
|
16
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
17
17
|
|
|
18
18
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
19
19
|
|
data/lib/dagger_ruby/config.rb
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
require "logger"
|
|
2
|
+
require_relative "version"
|
|
2
3
|
|
|
3
4
|
module DaggerRuby
|
|
4
5
|
class Config
|
|
5
|
-
|
|
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]
|
data/lib/dagger_ruby/errors.rb
CHANGED
|
@@ -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
|
data/lib/dagger_ruby/version.rb
CHANGED
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
|
-
|
|
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&.
|
|
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.
|
|
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.
|
|
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:
|
|
103
|
+
version: 3.2.0
|
|
102
104
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
105
|
requirements:
|
|
104
106
|
- - ">="
|