solid_queue_guard 1.0.0 → 1.0.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 +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +149 -2
- data/lib/solid_queue_guard/checks/config/queue_schema_check.rb +80 -7
- data/lib/solid_queue_guard/schema/solid_queue_tables.rb +126 -0
- data/lib/solid_queue_guard/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4708571f815d3044a3aa1f952ffe250699e8b695972cf8449b38f53aa1ff4f3
|
|
4
|
+
data.tar.gz: ef314fa6328235a4e8d5174fe42db21a200ebb8ab067b2b6cda125b8a67b433a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 292e090e0c051b8f76d792070bfef73a33e4a21ba680f23319d85354b9de145fa1363dbd1b74efd19c3b23cf5833595b8bb45720ba19a9f41ae9a1497161fd19
|
|
7
|
+
data.tar.gz: 64b1b5b2a1d9e5535ba1170f1469cdf3ac693a9f358b4386fb22699c1ad03c37aabd49176b66ca2c51a0cd78c57d2f447c87581a6ee2450ebb1e317d80d6295d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.1] - 2026-07-09
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `QueueSchemaCheck` validates Solid Queue tables in `db/queue_schema.rb`, `db/schema.rb`, `db/structure.sql`, and the queue database instead of requiring `db/queue_schema.rb`
|
|
13
|
+
- README documents operational usage: doctor, CI, HTTP health, and runtime process checks
|
|
14
|
+
|
|
8
15
|
## [1.0.0] - 2026-07-09
|
|
9
16
|
|
|
10
17
|
### Added
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Checks:
|
|
|
49
49
|
✅ Active Job adapter is :solid_queue
|
|
50
50
|
✅ Queue database configured in database.yml
|
|
51
51
|
✅ Solid Queue connects_to queue database (pool: 10)
|
|
52
|
-
✅
|
|
52
|
+
✅ Solid Queue schema tables present
|
|
53
53
|
❌ Worker threads: 10, queue DB pool: 5
|
|
54
54
|
⚠️ No workers configured for "mailers" queue
|
|
55
55
|
⚠️ recurring.yml exists but scheduler may not run
|
|
@@ -76,7 +76,7 @@ Runs locally, in CI, or pre-deploy. **No extra infrastructure.**
|
|
|
76
76
|
| **Adapter** | Active Job not set to `:solid_queue` |
|
|
77
77
|
| **Queue database** | Missing `queue` entry in `database.yml` |
|
|
78
78
|
| **connects_to** | Solid Queue pointing at the wrong DB / pool |
|
|
79
|
-
| **Queue schema** | Missing
|
|
79
|
+
| **Queue schema** | Missing Solid Queue tables in schema files or queue database |
|
|
80
80
|
| **Thread pool** | `threads` > available queue DB connections |
|
|
81
81
|
| **Worker coverage** | Queues with no worker assigned |
|
|
82
82
|
| **Scheduler** | `recurring.yml` tasks without a scheduler |
|
|
@@ -178,6 +178,153 @@ Works with **Kamal**, **Heroku**, **Fly.io**, **ECS/Fargate**, **Kubernetes**, *
|
|
|
178
178
|
|
|
179
179
|
---
|
|
180
180
|
|
|
181
|
+
## How to use it
|
|
182
|
+
|
|
183
|
+
Solid Queue does not show up in Rails `/up`. **solid_queue_guard** gives you three operational surfaces:
|
|
184
|
+
|
|
185
|
+
| Surface | Command / URL | Best for |
|
|
186
|
+
| ------- | ------------- | -------- |
|
|
187
|
+
| **Doctor** | `bin/rails solid_queue_guard:doctor` | Local pre-deploy, config review |
|
|
188
|
+
| **CI gate** | `SOLID_QUEUE_GUARD_STRICT=1 bin/rails solid_queue_guard:doctor` | Block merges with broken queue config |
|
|
189
|
+
| **HTTP health** | `GET /solid_queue_guard/health` | Production uptime monitors (Kamal, ECS, UptimeRobot) |
|
|
190
|
+
|
|
191
|
+
**Mission Control** shows what is happening. **solid_queue_guard** warns what is dangerous. Use both.
|
|
192
|
+
|
|
193
|
+
### Local and pre-deploy
|
|
194
|
+
|
|
195
|
+
Run before changing `queue.yml`, `database.yml`, or recurring tasks:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bin/rails solid_queue_guard:doctor # config checks (default scope)
|
|
199
|
+
bin/rails solid_queue_guard:report # config + runtime when the queue DB is available
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
By default the install generator sets `config.enabled = Rails.env.production?`, so in **development** checks are skipped unless you enable them:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
SolidQueueGuard.configure { |c| c.enabled = true }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Or run with production config locally when validating deploy readiness.
|
|
209
|
+
|
|
210
|
+
### CI pipelines
|
|
211
|
+
|
|
212
|
+
Validate **configuration** before deploy. Process/runtime checks usually **skip** in CI because there is no `bin/jobs` supervisor on the runner.
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
- name: Solid Queue production readiness
|
|
216
|
+
run: SOLID_QUEUE_GUARD_STRICT=1 bin/rails solid_queue_guard:doctor
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`STRICT=1` turns warnings into exit code `1`, so the pipeline fails on misconfigured pools, missing worker coverage, or missing schema tables — not only hard failures.
|
|
220
|
+
|
|
221
|
+
Generate a starter workflow:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
bin/rails generate solid_queue_guard:install:ci
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Production monitoring
|
|
228
|
+
|
|
229
|
+
Mount the engine and point your load balancer or Kamal health check at `/solid_queue_guard/health`:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
# config/routes.rb
|
|
233
|
+
mount SolidQueueGuard::Engine, at: "/solid_queue_guard"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Runtime checks matter here: queue lag, stale heartbeats, dispatcher health, and process topology reflect **live** Solid Queue state.
|
|
237
|
+
|
|
238
|
+
Optional hardening:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
config.health_token = ENV["SOLID_QUEUE_GUARD_TOKEN"]
|
|
242
|
+
config.health_cache_ttl = 15.seconds
|
|
243
|
+
config.degraded_http_status = 207 # or :ok (200), 503, etc.
|
|
244
|
+
config.notify_with = [:rails_logger, :slack]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Typical flow
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
Developer CI Production
|
|
251
|
+
│ │ │
|
|
252
|
+
│ doctor │ doctor --strict │ GET /health (every 30–60s)
|
|
253
|
+
▼ ▼ ▼
|
|
254
|
+
"pool wrong?" block bad deploy alert: worker dead
|
|
255
|
+
"schema ok?" before merge alert: queue lag
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Runtime process checks
|
|
261
|
+
|
|
262
|
+
Runtime checks query the **queue database** (via `SolidQueue::Record`). If `connects_to` is missing or the DB is unreachable, they **skip** — they do not pass silently as healthy.
|
|
263
|
+
|
|
264
|
+
### `process_topology` — are the expected roles present?
|
|
265
|
+
|
|
266
|
+
Reads distinct `kind` values from `solid_queue_processes`:
|
|
267
|
+
|
|
268
|
+
| Kind | Role |
|
|
269
|
+
| ---- | ---- |
|
|
270
|
+
| `Supervisor` | Parent process (`bin/jobs`) |
|
|
271
|
+
| `Worker` | Consumes ready jobs |
|
|
272
|
+
| `Dispatcher` | Moves scheduled jobs to ready |
|
|
273
|
+
| `Scheduler` | Runs recurring tasks |
|
|
274
|
+
|
|
275
|
+
| Result | Meaning |
|
|
276
|
+
| ------ | ------- |
|
|
277
|
+
| ⚠️ No processes | Nothing registered — supervisor not running |
|
|
278
|
+
| ⚠️ No Worker | Jobs will not be processed |
|
|
279
|
+
| ⚠️ No Dispatcher | Scheduled work may stall (when recurring/scheduled jobs exist) |
|
|
280
|
+
| ✅ Pass | Expected kinds are present |
|
|
281
|
+
|
|
282
|
+
This check looks at **presence of records**, not heartbeat freshness.
|
|
283
|
+
|
|
284
|
+
### `stale_process` — are heartbeats fresh?
|
|
285
|
+
|
|
286
|
+
Finds processes where `last_heartbeat_at` is older than `stale_process_threshold` (default **5 minutes**):
|
|
287
|
+
|
|
288
|
+
| Result | Meaning |
|
|
289
|
+
| ------ | ------- |
|
|
290
|
+
| ✅ Pass | All processes reported recently |
|
|
291
|
+
| ❌ Fail | One or more workers/dispatchers look dead or stuck |
|
|
292
|
+
|
|
293
|
+
Use this in production health to catch workers that died after deploy.
|
|
294
|
+
|
|
295
|
+
### `pidfile` — optional supervisor liveness
|
|
296
|
+
|
|
297
|
+
When `tmp/pids/solid_queue.pid` (or `SOLID_QUEUE_PIDFILE`) exists, verifies the PID is alive. Often **warns in development** where `bin/jobs` is not running. Disable if you do not use pidfiles:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
config.disabled_checks = [:pidfile]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### `puma_plugin_runtime` — Solid Queue inside Puma
|
|
304
|
+
|
|
305
|
+
When `plugin :solid_queue` is in `config/puma.rb`, verifies active processes with recent heartbeats exist. **Skips** when the Puma plugin is not enabled.
|
|
306
|
+
|
|
307
|
+
### Queue schema detection
|
|
308
|
+
|
|
309
|
+
`QueueSchemaCheck` does **not** require `db/queue_schema.rb`. It validates that all tables for your installed **solid_queue** version exist in any of:
|
|
310
|
+
|
|
311
|
+
- `db/queue_schema.rb`
|
|
312
|
+
- `db/schema.rb`
|
|
313
|
+
- `db/structure.sql`
|
|
314
|
+
- the connected queue database
|
|
315
|
+
|
|
316
|
+
Apps that keep Solid Queue tables only in `structure.sql` (common in Revelo-style repos) pass correctly.
|
|
317
|
+
|
|
318
|
+
### Where each check type applies
|
|
319
|
+
|
|
320
|
+
| Context | Config checks | Process / runtime checks |
|
|
321
|
+
| ------- | ------------- | ------------------------ |
|
|
322
|
+
| Local `doctor` | ✅ Primary value | ⚠️ Partial without `bin/jobs` |
|
|
323
|
+
| CI `--strict` | ✅ Primary value | ⏭️ Usually skip |
|
|
324
|
+
| Production `/health` | ✅ When DB up | ✅ Primary value |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
181
328
|
## Configuration
|
|
182
329
|
|
|
183
330
|
```ruby
|
|
@@ -5,18 +5,91 @@ module SolidQueueGuard
|
|
|
5
5
|
module Config
|
|
6
6
|
class QueueSchemaCheck < Base
|
|
7
7
|
def call
|
|
8
|
-
|
|
8
|
+
evaluation = evaluate_schema
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
case evaluation[:status]
|
|
11
|
+
when :pass
|
|
12
|
+
pass(check_id, evaluation[:message], metadata: evaluation[:metadata])
|
|
13
|
+
when :warn
|
|
13
14
|
warn(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
suggestion:
|
|
15
|
+
check_id,
|
|
16
|
+
evaluation[:message],
|
|
17
|
+
suggestion: evaluation[:suggestion],
|
|
18
|
+
metadata: evaluation[:metadata]
|
|
19
|
+
)
|
|
20
|
+
else
|
|
21
|
+
failure(
|
|
22
|
+
check_id,
|
|
23
|
+
evaluation[:message],
|
|
24
|
+
suggestion: evaluation[:suggestion],
|
|
25
|
+
metadata: evaluation[:metadata]
|
|
17
26
|
)
|
|
18
27
|
end
|
|
19
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def evaluate_schema
|
|
33
|
+
required = Schema::SolidQueueTables.required_tables
|
|
34
|
+
detection = Schema::SolidQueueTables.detected_tables(rails_root: rails_root)
|
|
35
|
+
missing = required - detection[:tables]
|
|
36
|
+
metadata = schema_metadata(detection, missing: missing)
|
|
37
|
+
|
|
38
|
+
return pass_evaluation(detection, required.size, metadata) if missing.empty?
|
|
39
|
+
return partial_evaluation(missing, metadata) if detection[:tables].any?
|
|
40
|
+
|
|
41
|
+
empty_evaluation(metadata)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pass_evaluation(detection, table_count, metadata)
|
|
45
|
+
{
|
|
46
|
+
status: :pass,
|
|
47
|
+
message: success_message(detection, table_count),
|
|
48
|
+
metadata: metadata
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def partial_evaluation(missing, metadata)
|
|
53
|
+
{
|
|
54
|
+
status: :warn,
|
|
55
|
+
message: "Missing Solid Queue tables: #{missing.join(', ')}",
|
|
56
|
+
suggestion: 'Run bin/rails solid_queue:install or migrate the queue database',
|
|
57
|
+
metadata: metadata
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def empty_evaluation(metadata)
|
|
62
|
+
{
|
|
63
|
+
status: :fail,
|
|
64
|
+
message: [
|
|
65
|
+
'No Solid Queue schema tables found in',
|
|
66
|
+
'db/queue_schema.rb, db/schema.rb, db/structure.sql, or the queue database'
|
|
67
|
+
].join(' '),
|
|
68
|
+
suggestion: 'Run bin/rails solid_queue:install and migrate the queue database',
|
|
69
|
+
metadata: metadata
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def success_message(detection, table_count)
|
|
74
|
+
sources = source_descriptions(detection)
|
|
75
|
+
"Solid Queue schema complete (#{table_count}/#{table_count} tables) in #{sources.join(', ')}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def source_descriptions(detection)
|
|
79
|
+
sources = detection[:file_tables].keys
|
|
80
|
+
sources << 'queue database' if detection[:database_tables].any?
|
|
81
|
+
sources.presence || ['schema files']
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def schema_metadata(detection, missing:)
|
|
85
|
+
{
|
|
86
|
+
required_tables: Schema::SolidQueueTables.required_tables,
|
|
87
|
+
detected_tables: detection[:tables],
|
|
88
|
+
missing_tables: missing,
|
|
89
|
+
schema_sources: detection[:file_tables],
|
|
90
|
+
database_tables: detection[:database_tables]
|
|
91
|
+
}
|
|
92
|
+
end
|
|
20
93
|
end
|
|
21
94
|
end
|
|
22
95
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Schema
|
|
5
|
+
# @api private
|
|
6
|
+
class SolidQueueTables
|
|
7
|
+
SCHEMA_FILES = %w[
|
|
8
|
+
db/queue_schema.rb
|
|
9
|
+
db/schema.rb
|
|
10
|
+
db/structure.sql
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
RUBY_CREATE_TABLE_PATTERN = /
|
|
14
|
+
create_table
|
|
15
|
+
\s+
|
|
16
|
+
(?:
|
|
17
|
+
["'](?<quoted>solid_queue_[a-z_]+)["']
|
|
18
|
+
|
|
|
19
|
+
:(?<symbol>solid_queue_[a-z_]+)
|
|
20
|
+
)
|
|
21
|
+
/ix
|
|
22
|
+
|
|
23
|
+
SQL_CREATE_TABLE_PATTERN = /
|
|
24
|
+
CREATE \ TABLE
|
|
25
|
+
(?: \ IF \ NOT \ EXISTS )?
|
|
26
|
+
\s+
|
|
27
|
+
(?:[\w.]+\.)?
|
|
28
|
+
(?:
|
|
29
|
+
"(?<quoted>solid_queue_[a-z_]+)"
|
|
30
|
+
|
|
|
31
|
+
(?<unquoted>solid_queue_[a-z_]+)
|
|
32
|
+
)
|
|
33
|
+
/ix
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def required_tables
|
|
37
|
+
@required_tables ||= tables_from_gem_template || default_required_tables
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detected_tables(rails_root: Rails.root)
|
|
41
|
+
file_tables = {}
|
|
42
|
+
tables = Set.new
|
|
43
|
+
|
|
44
|
+
SCHEMA_FILES.each do |relative_path|
|
|
45
|
+
path = rails_root.join(relative_path)
|
|
46
|
+
next unless path.exist?
|
|
47
|
+
|
|
48
|
+
tables_in_file = parse_file(path)
|
|
49
|
+
next if tables_in_file.empty?
|
|
50
|
+
|
|
51
|
+
file_tables[relative_path] = tables_in_file.sort
|
|
52
|
+
tables.merge(tables_in_file)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
database_tables = tables_in_queue_database
|
|
56
|
+
tables.merge(database_tables)
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
tables: tables.sort,
|
|
60
|
+
file_tables: file_tables,
|
|
61
|
+
database_tables: database_tables.sort
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def missing_tables(rails_root: Rails.root)
|
|
66
|
+
required_tables - detected_tables(rails_root: rails_root)[:tables]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def tables_from_gem_template
|
|
70
|
+
spec = Gem.loaded_specs['solid_queue']
|
|
71
|
+
return nil unless spec
|
|
72
|
+
|
|
73
|
+
template = File.join(
|
|
74
|
+
spec.full_gem_path,
|
|
75
|
+
'lib/generators/solid_queue/install/templates/db/queue_schema.rb'
|
|
76
|
+
)
|
|
77
|
+
return nil unless File.exist?(template)
|
|
78
|
+
|
|
79
|
+
parse_ruby_schema(File.read(template))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def default_required_tables
|
|
83
|
+
%w[
|
|
84
|
+
solid_queue_blocked_executions
|
|
85
|
+
solid_queue_claimed_executions
|
|
86
|
+
solid_queue_failed_executions
|
|
87
|
+
solid_queue_jobs
|
|
88
|
+
solid_queue_pauses
|
|
89
|
+
solid_queue_processes
|
|
90
|
+
solid_queue_ready_executions
|
|
91
|
+
solid_queue_recurring_executions
|
|
92
|
+
solid_queue_recurring_tasks
|
|
93
|
+
solid_queue_scheduled_executions
|
|
94
|
+
solid_queue_semaphores
|
|
95
|
+
].freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def parse_file(path)
|
|
101
|
+
content = path.read
|
|
102
|
+
path.extname == '.sql' ? parse_sql_schema(content) : parse_ruby_schema(content)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_ruby_schema(content)
|
|
106
|
+
content.scan(RUBY_CREATE_TABLE_PATTERN).flatten.compact.uniq
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_sql_schema(content)
|
|
110
|
+
content.scan(SQL_CREATE_TABLE_PATTERN).flatten.compact.uniq
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def tables_in_queue_database
|
|
114
|
+
return [] unless defined?(SolidQueue::Record)
|
|
115
|
+
|
|
116
|
+
pool = SolidQueue::Record.connection_pool
|
|
117
|
+
return [] unless pool&.with_connection(&:active?)
|
|
118
|
+
|
|
119
|
+
SolidQueue::Record.connection.tables.grep(/\Asolid_queue_/)
|
|
120
|
+
rescue StandardError
|
|
121
|
+
[]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rafael Pissardo
|
|
@@ -337,6 +337,7 @@ files:
|
|
|
337
337
|
- lib/solid_queue_guard/recommendations/topology.rb
|
|
338
338
|
- lib/solid_queue_guard/report.rb
|
|
339
339
|
- lib/solid_queue_guard/runner.rb
|
|
340
|
+
- lib/solid_queue_guard/schema/solid_queue_tables.rb
|
|
340
341
|
- lib/solid_queue_guard/tasks.rb
|
|
341
342
|
- lib/solid_queue_guard/version.rb
|
|
342
343
|
- solid_queue_guard.gemspec
|