boring_services 0.8.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +9 -3
- data/SECURITY.md +22 -0
- data/lib/boring_services/cli.rb +5 -4
- data/lib/boring_services/installer.rb +5 -5
- data/lib/boring_services/secrets.rb +21 -12
- data/lib/boring_services/service_locator.rb +1 -1
- data/lib/boring_services/services/haproxy.rb +2 -2
- data/lib/boring_services/services/redis.rb +2 -4
- data/lib/boring_services/ssh_executor.rb +2 -2
- data/lib/boring_services/version.rb +1 -1
- data/lib/boring_services.rb +1 -1
- metadata +16 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d26c6e6770c20727d68f773f48f1505b9eaeedef20b72d73cd9af76668773bc4
|
|
4
|
+
data.tar.gz: b6d7d7ca8c099f958c51f542ff0f7eb64d6b23ae1241ca62e27041c044867f1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eab7cdbc819793e3f49ebb6a2212f37cd5d4c2019d8d96b15e70ca9431732a823ec51a689c450a9f8fd9b2fa8e3e7db849d716428044c988e697701bb36b45f6
|
|
7
|
+
data.tar.gz: 23d2c7f46e5e6a14a1f6adf143771a1869e5dbb66401f7ac175ba70797b0c70ba0a4b114ac5cede972f2dd68c6ace88a46ecd2258033a1226533c0579b461519
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.8.1] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Require Ruby 4.0 or newer.
|
|
15
|
+
- Align runtime and development dependencies with the maintained toolchain.
|
|
16
|
+
- Add blocking test, lint, dependency-audit, and package-build CI checks.
|
|
17
|
+
- Add automated Bundler and GitHub Actions dependency updates.
|
|
18
|
+
- Clarify that private IP metadata does not configure bind addresses or
|
|
19
|
+
firewalls.
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2025-01-21
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Initial release of BoringServices
|
|
25
|
+
- Deploy infrastructure services (Memcached, Redis, HAProxy, Nginx) via SSH
|
|
26
|
+
- Rails integration with generators and rake tasks
|
|
27
|
+
- Standalone Ruby support (no Rails dependency required)
|
|
28
|
+
- Configuration override support for HAProxy via `custom_params`:
|
|
29
|
+
- `timeout_connect`, `timeout_client`, `timeout_server`
|
|
30
|
+
- `balance` algorithm (roundrobin, leastconn, etc.)
|
|
31
|
+
- `health_check_path` customization
|
|
32
|
+
- `ssl_ciphers` and `ssl_options` for SSL/TLS configuration
|
|
33
|
+
- Configuration override support for Memcached via `custom_params`:
|
|
34
|
+
- `listen_address`, `max_connections`
|
|
35
|
+
- `max_item_size`, `verbosity`
|
|
36
|
+
- Custom config template support:
|
|
37
|
+
- HAProxy: `custom_config_template` option to provide your own config file
|
|
38
|
+
- Memcached: `custom_config_template` option to provide your own config file
|
|
39
|
+
- VPN/private network support with `private_ip` field for tracking internal IPs
|
|
40
|
+
- Multi-host deployment support for services
|
|
41
|
+
- Environment-based configuration (development, staging, production)
|
|
42
|
+
- Secret management via environment variables or command execution
|
|
43
|
+
- SSL/TLS support for HAProxy with automatic certificate management
|
|
44
|
+
- Health checking and status monitoring
|
|
45
|
+
- SSH key-based authentication with configurable options
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Made railties dependency optional for standalone Ruby usage
|
|
49
|
+
- Improved documentation with comprehensive examples
|
|
50
|
+
- Updated Ruby requirement to >= 3.0.0 (from >= 3.4.0)
|
|
51
|
+
|
|
52
|
+
### Technical Details
|
|
53
|
+
- Uses SSHKit for SSH operations
|
|
54
|
+
- Supports Ubuntu 20.04+ servers (Debian-based distributions)
|
|
55
|
+
- YAML-based configuration with ERB support
|
|
56
|
+
- Thor-based CLI interface
|
data/README.md
CHANGED
|
@@ -371,9 +371,15 @@ BoringServices supports deploying services over VPN or private networks (e.g., W
|
|
|
371
371
|
|
|
372
372
|
- **Public IPs** (`host`) for SSH deployment access
|
|
373
373
|
- **Private IPs** (`private_ip`) for service communication
|
|
374
|
-
-
|
|
374
|
+
- Some generated service configurations listen on all interfaces (`0.0.0.0`)
|
|
375
|
+
by default
|
|
375
376
|
- Accessible via private network IPs
|
|
376
|
-
-
|
|
377
|
+
- `private_ip` documents the address clients should use; it does not change the
|
|
378
|
+
service bind address or configure a firewall
|
|
379
|
+
|
|
380
|
+
Before deploying, restrict each service port to the intended private CIDRs with
|
|
381
|
+
the provider firewall and the host firewall, or set a supported service-specific
|
|
382
|
+
listen address. A public SSH address does not make a service endpoint private.
|
|
377
383
|
|
|
378
384
|
**Example with Private IPs:**
|
|
379
385
|
|
|
@@ -428,7 +434,7 @@ haproxy: healthy
|
|
|
428
434
|
|
|
429
435
|
## Requirements
|
|
430
436
|
|
|
431
|
-
- Ruby
|
|
437
|
+
- Ruby 4.0+
|
|
432
438
|
- Ubuntu 20.04+ servers (Debian-based distributions)
|
|
433
439
|
- SSH key-based authentication
|
|
434
440
|
- Optional: VPN solution (WireGuard, Tailscale, etc.) for private networking
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not open a public issue for a potential vulnerability. Email
|
|
6
|
+
[security@boringcache.com](mailto:security@boringcache.com) or use
|
|
7
|
+
[GitHub private vulnerability reporting](https://github.com/boringcache/services/security/advisories/new).
|
|
8
|
+
|
|
9
|
+
Include the affected version, service, expected impact, and enough reproduction
|
|
10
|
+
detail for us to investigate. Do not include live credentials, private keys,
|
|
11
|
+
certificates, customer data, or production configuration.
|
|
12
|
+
|
|
13
|
+
## Supported Versions
|
|
14
|
+
|
|
15
|
+
Only the latest published release is supported. Security fixes are normally
|
|
16
|
+
shipped in a new release rather than backported.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Reports about SSH host verification, network exposure, secret handling,
|
|
21
|
+
generated service configuration, command execution, or release integrity are
|
|
22
|
+
welcome.
|
data/lib/boring_services/cli.rb
CHANGED
|
@@ -52,7 +52,8 @@ module BoringServices
|
|
|
52
52
|
exit 1
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
desc 'reconfigure [SERVICE]',
|
|
55
|
+
desc 'reconfigure [SERVICE]',
|
|
56
|
+
'Reconfigure service(s) - skips package installation, only updates config and restarts'
|
|
56
57
|
def reconfigure(service_name = nil)
|
|
57
58
|
config = Configuration.load(options[:config], options[:environment])
|
|
58
59
|
installer = Installer.new(config)
|
|
@@ -108,7 +109,7 @@ module BoringServices
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def print_memcached_stats(stats)
|
|
111
|
-
bytes = format_bytes(stats['bytes']
|
|
112
|
+
bytes = format_bytes(stats['bytes'].to_i)
|
|
112
113
|
conns = stats['curr_connections'] || '0'
|
|
113
114
|
items = stats['curr_items'] || '0'
|
|
114
115
|
hit_rate = calculate_hit_rate(stats['get_hits'], stats['get_misses'])
|
|
@@ -116,8 +117,8 @@ module BoringServices
|
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
def calculate_hit_rate(hits, misses)
|
|
119
|
-
hits = hits
|
|
120
|
-
misses = misses
|
|
120
|
+
hits = hits.to_i
|
|
121
|
+
misses = misses.to_i
|
|
121
122
|
total = hits + misses
|
|
122
123
|
total.positive? ? ((hits.to_f / total) * 100).round(1) : 0
|
|
123
124
|
end
|
|
@@ -115,9 +115,9 @@ module BoringServices
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def print_credentials_summary(hints)
|
|
118
|
-
puts "\n
|
|
119
|
-
puts
|
|
120
|
-
puts
|
|
118
|
+
puts "\n#{'=' * 50}"
|
|
119
|
+
puts '📋 Add to Rails credentials:'
|
|
120
|
+
puts '=' * 50
|
|
121
121
|
|
|
122
122
|
# Group by service type
|
|
123
123
|
grouped = hints.group_by { |h| h[:type] }
|
|
@@ -129,14 +129,14 @@ module BoringServices
|
|
|
129
129
|
by_region = entries.group_by { |e| e[:region] }
|
|
130
130
|
by_region.each do |region, region_entries|
|
|
131
131
|
puts " #{region}:"
|
|
132
|
-
puts
|
|
132
|
+
puts ' servers:'
|
|
133
133
|
region_entries.each do |entry|
|
|
134
134
|
puts " - #{entry[:server]} # #{entry[:label]}"
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
puts "\n
|
|
139
|
+
puts "\n#{'=' * 50}"
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
require 'English'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
2
5
|
module BoringServices
|
|
3
6
|
class Secrets
|
|
4
7
|
def self.resolve(value)
|
|
@@ -8,10 +11,10 @@ module BoringServices
|
|
|
8
11
|
|
|
9
12
|
if value_str.start_with?('credentials:')
|
|
10
13
|
resolve_rails_credentials(value_str)
|
|
11
|
-
elsif value_str.start_with?('$')
|
|
12
|
-
resolve_env_var(value_str)
|
|
13
14
|
elsif value_str.start_with?('$(') && value_str.end_with?(')')
|
|
14
15
|
resolve_command(value_str[2..-2])
|
|
16
|
+
elsif value_str.start_with?('$')
|
|
17
|
+
resolve_env_var(value_str)
|
|
15
18
|
else
|
|
16
19
|
value_str
|
|
17
20
|
end
|
|
@@ -24,16 +27,19 @@ module BoringServices
|
|
|
24
27
|
|
|
25
28
|
# Use rails credentials:show to fetch the value
|
|
26
29
|
# This requires being run from the Rails root directory
|
|
27
|
-
|
|
28
|
-
result =
|
|
30
|
+
runner = "puts Rails.application.credentials.dig(#{keys.map { |key| key.to_sym.inspect }.join(', ')})"
|
|
31
|
+
result, stderr, status = Open3.capture3(
|
|
32
|
+
{ 'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'production') },
|
|
33
|
+
'bundle', 'exec', 'rails', 'runner', runner
|
|
34
|
+
)
|
|
29
35
|
|
|
30
|
-
unless
|
|
31
|
-
|
|
36
|
+
unless status.success?
|
|
37
|
+
detail = stderr.strip.empty? ? 'Make sure you are running from the Rails root directory.' : stderr.strip
|
|
38
|
+
raise Error, "Failed to resolve Rails credentials: #{key_path}. #{detail}"
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
41
|
+
result = result.strip
|
|
42
|
+
raise Error, "Rails credentials key not found: #{key_path}" if result.empty?
|
|
37
43
|
|
|
38
44
|
result
|
|
39
45
|
end
|
|
@@ -46,10 +52,13 @@ module BoringServices
|
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
def self.resolve_command(command)
|
|
49
|
-
|
|
50
|
-
raise Error,
|
|
55
|
+
argv = Shellwords.split(command)
|
|
56
|
+
raise Error, 'Command secret is empty' if argv.empty?
|
|
51
57
|
|
|
52
|
-
result
|
|
58
|
+
result, stderr, status = Open3.capture3(*argv)
|
|
59
|
+
raise Error, "Command failed: #{argv.first}: #{stderr.strip}" unless status.success?
|
|
60
|
+
|
|
61
|
+
result.strip
|
|
53
62
|
end
|
|
54
63
|
end
|
|
55
64
|
end
|
|
@@ -9,7 +9,7 @@ module BoringServices
|
|
|
9
9
|
configure_haproxy
|
|
10
10
|
validate_config
|
|
11
11
|
ssh_executor.systemd_enable('haproxy')
|
|
12
|
-
ssh_executor.systemd_restart('haproxy')
|
|
12
|
+
ssh_executor.systemd_restart('haproxy') # Use restart instead of start to reload config
|
|
13
13
|
verify_listening_ports
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -257,7 +257,7 @@ module BoringServices
|
|
|
257
257
|
ports_to_check << stats_port
|
|
258
258
|
|
|
259
259
|
ports_to_check.each do |check_port|
|
|
260
|
-
result = execute :sudo, :ss, '-tlnp', '|', :grep,
|
|
260
|
+
result = execute :sudo, :ss, '-tlnp', '|', :grep, '-E', "':#{check_port} '", raise_on_error: false
|
|
261
261
|
if result
|
|
262
262
|
puts " ✓ Port #{check_port} is listening"
|
|
263
263
|
else
|
|
@@ -42,7 +42,7 @@ module BoringServices
|
|
|
42
42
|
listen_port = port || 6379
|
|
43
43
|
password = resolve_secret('redis_password') if config.secrets['redis_password']
|
|
44
44
|
has_private_ip = !private_ip.to_s.strip.empty?
|
|
45
|
-
bind_address = has_private_ip ? "127.0.0.1 #{private_ip}" :
|
|
45
|
+
bind_address = has_private_ip ? "127.0.0.1 #{private_ip}" : '0.0.0.0'
|
|
46
46
|
|
|
47
47
|
config_content = <<~REDIS
|
|
48
48
|
bind #{bind_address}
|
|
@@ -62,9 +62,7 @@ module BoringServices
|
|
|
62
62
|
execute :sudo, :chmod, '640', '/etc/redis/redis.conf'
|
|
63
63
|
|
|
64
64
|
# If using private IP (WireGuard), ensure Redis starts after WireGuard
|
|
65
|
-
if has_private_ip
|
|
66
|
-
configure_wireguard_dependency
|
|
67
|
-
end
|
|
65
|
+
configure_wireguard_dependency if has_private_ip
|
|
68
66
|
|
|
69
67
|
# Set vm.overcommit_memory for Redis background saves
|
|
70
68
|
execute :sudo, :sysctl, '-w', 'vm.overcommit_memory=1'
|
|
@@ -152,10 +152,10 @@ module BoringServices
|
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
def capture_on_host(host, *
|
|
155
|
+
def capture_on_host(host, *)
|
|
156
156
|
output = nil
|
|
157
157
|
execute_on_host(host) do
|
|
158
|
-
output = capture(
|
|
158
|
+
output = capture(*, raise_on_non_zero_exit: false)
|
|
159
159
|
end
|
|
160
160
|
output
|
|
161
161
|
end
|
data/lib/boring_services.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boring_services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -29,84 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.
|
|
32
|
+
version: '1.4'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.
|
|
39
|
+
version: '1.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: sshkit
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.
|
|
46
|
+
version: '1.25'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.
|
|
53
|
+
version: '1.25'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: thor
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '1.
|
|
60
|
+
version: '1.5'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: minitest
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '5.0'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '5.0'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: rubocop
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '1.50'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '1.50'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: simplecov
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - "~>"
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.22'
|
|
103
|
-
type: :development
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - "~>"
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.22'
|
|
67
|
+
version: '1.5'
|
|
110
68
|
description: Simple deployment and management of infrastructure services like Memcached,
|
|
111
69
|
Redis, HAProxy, and Nginx. Works standalone or with Rails.
|
|
112
70
|
email:
|
|
@@ -116,8 +74,10 @@ executables:
|
|
|
116
74
|
extensions: []
|
|
117
75
|
extra_rdoc_files: []
|
|
118
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
119
78
|
- LICENSE
|
|
120
79
|
- README.md
|
|
80
|
+
- SECURITY.md
|
|
121
81
|
- exe/boringservices
|
|
122
82
|
- lib/boring_services.rb
|
|
123
83
|
- lib/boring_services/cli.rb
|
|
@@ -136,14 +96,14 @@ files:
|
|
|
136
96
|
- lib/boring_services/ssh_executor.rb
|
|
137
97
|
- lib/boring_services/version.rb
|
|
138
98
|
- lib/tasks/services.rake
|
|
139
|
-
homepage: https://github.com/boringcache/
|
|
99
|
+
homepage: https://github.com/boringcache/services
|
|
140
100
|
licenses:
|
|
141
101
|
- MIT
|
|
142
102
|
metadata:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
103
|
+
source_code_uri: https://github.com/boringcache/services
|
|
104
|
+
documentation_uri: https://github.com/boringcache/services/blob/main/README.md
|
|
105
|
+
changelog_uri: https://github.com/boringcache/services/blob/main/CHANGELOG.md
|
|
106
|
+
bug_tracker_uri: https://github.com/boringcache/services/issues
|
|
147
107
|
rubygems_mfa_required: 'true'
|
|
148
108
|
rdoc_options: []
|
|
149
109
|
require_paths:
|
|
@@ -152,14 +112,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
152
112
|
requirements:
|
|
153
113
|
- - ">="
|
|
154
114
|
- !ruby/object:Gem::Version
|
|
155
|
-
version:
|
|
115
|
+
version: 4.0.0
|
|
156
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
117
|
requirements:
|
|
158
118
|
- - ">="
|
|
159
119
|
- !ruby/object:Gem::Version
|
|
160
120
|
version: '0'
|
|
161
121
|
requirements: []
|
|
162
|
-
rubygems_version:
|
|
122
|
+
rubygems_version: 4.0.16
|
|
163
123
|
specification_version: 4
|
|
164
124
|
summary: Deploy infrastructure services for Ruby & Rails apps
|
|
165
125
|
test_files: []
|