deployangel 0.1.19 → 0.1.21
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 +17 -0
- data/README.md +25 -13
- data/lib/deployangel/cli.rb +62 -6
- data/lib/deployangel/client.rb +6 -0
- data/lib/deployangel/exerciser.rb +139 -0
- data/lib/deployangel/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: 2a115a5a35d86c5edce65dad1cdf3947833416ea66f47c69da18b85de3047153
|
|
4
|
+
data.tar.gz: ec954e62605c6452d3e374896085343cb6a12910e0b821794342a2e0a6dc0426
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c18ca61ecff8936bdfa7f4a5900fd27b96c2a787094e504593fae78f62eaee3c7d4ea11e74811b619313c1a726bce8e4644f5a6319c687450ff81629acdd2a5
|
|
7
|
+
data.tar.gz: 2754b3cc315c626df0dc57de4723721caa668f69b7b523a0ca4eaecaf4f84e2f2ec7f9f40f366be34f9a1989ff3c9b4650f1d02dc81b976218d2b3daf5e3968c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.21 (2026-10-09)
|
|
4
|
+
|
|
5
|
+
- `deployangel exercise` stops requesting a page after its first 404, 405,
|
|
6
|
+
or 410, since a route table can list pages an app doesn't serve, and sends
|
|
7
|
+
that page's share to the pages that answered.
|
|
8
|
+
|
|
9
|
+
## 0.1.20 (2026-10-09)
|
|
10
|
+
|
|
11
|
+
- `deployangel exercise --url=<production URL>` sends a release's exercise
|
|
12
|
+
plan's read-only requests (GET routes without path parameters, at most
|
|
13
|
+
200, about 5 a second) so a quiet release can clear sooner, then records
|
|
14
|
+
what it sent on the release, which lists it as exercised from your side.
|
|
15
|
+
It skips and names routes that change data or need a path parameter.
|
|
16
|
+
`--dry-run` shows what it would send.
|
|
17
|
+
- The agent instructions `deployangel install agents` writes now tell the
|
|
18
|
+
agent to run `deployangel exercise` when a release isn't cleared yet.
|
|
19
|
+
|
|
3
20
|
## 0.1.19 (2026-10-09)
|
|
4
21
|
|
|
5
22
|
- `deployangel install agents` sets up Claude Code, Cursor, and Codex in a
|
data/README.md
CHANGED
|
@@ -392,6 +392,7 @@ bundle exec deployangel verify --wait # current git HEAD, until
|
|
|
392
392
|
bundle exec deployangel verify --wait --until=initial # return at the 15-minute initial check
|
|
393
393
|
bundle exec deployangel status # latest deployment
|
|
394
394
|
bundle exec deployangel plan # what to exercise so a release clears sooner
|
|
395
|
+
bundle exec deployangel exercise --url=https://example.com # send the plan's read-only requests
|
|
395
396
|
bundle exec deployangel release --commit=$SHA # register a deploy (manual or CI)
|
|
396
397
|
bundle exec deployangel check --name="smoke: signup" --status=pass --covers=registration
|
|
397
398
|
bundle exec deployangel exception <fingerprint>
|
|
@@ -464,16 +465,28 @@ check labels what it covered; only the requests themselves count as
|
|
|
464
465
|
evidence. A release deployed during an app's first 24 hours can't clear,
|
|
465
466
|
so its plan lists nothing.
|
|
466
467
|
|
|
468
|
+
`deployangel exercise --url=<production URL>` does the read-only part for
|
|
469
|
+
you, from wherever it runs: it requests the plan's GET routes that have no
|
|
470
|
+
path parameters, spreading any request shortfall across them (at most 200
|
|
471
|
+
requests, about 5 a second, as `DeployAngel-Exercise`), then records what it
|
|
472
|
+
sent on the release, where the page lists it under "Exercised from your
|
|
473
|
+
side". It skips and names routes that change data or need a path
|
|
474
|
+
parameter, and stops requesting a page after its first 404, sending its
|
|
475
|
+
share to the pages that answered. A "CLI & coding agents" token can run
|
|
476
|
+
it. `--dry-run` shows what it would send.
|
|
477
|
+
|
|
467
478
|
### Suggested instructions for your coding agent
|
|
468
479
|
|
|
469
480
|
`deployangel install agents` adds instructions like these. To add them
|
|
470
481
|
yourself, put this in your `CLAUDE.md` or `AGENTS.md`:
|
|
471
482
|
|
|
472
483
|
```markdown
|
|
473
|
-
## Production verification
|
|
484
|
+
## Production verification (DeployAngel)
|
|
474
485
|
|
|
475
|
-
After deploying,
|
|
476
|
-
|
|
486
|
+
After deploying, or pushing a commit that deploys, wait for DeployAngel's
|
|
487
|
+
verdict: call the `wait_for_verification` MCP tool with the commit and
|
|
488
|
+
`until: "initial"`, calling again while it's in progress, or run
|
|
489
|
+
`bundle exec deployangel verify --commit=<sha> --wait --until=initial`.
|
|
477
490
|
|
|
478
491
|
- Exit 0 / verified: the release is cleared. Report the clearance line and
|
|
479
492
|
anything DeployAngel is still watching, then move on.
|
|
@@ -483,17 +496,16 @@ After deploying, run `bundle exec deployangel verify --wait --until initial`
|
|
|
483
496
|
- Exit 7: warnings at the initial check. Report them and review the findings.
|
|
484
497
|
The release is NOT cleared.
|
|
485
498
|
- Exit 2 / inconclusive: the release is NOT verified. Do not claim success.
|
|
486
|
-
- Exit 1 / failed: read the findings and exceptions,
|
|
487
|
-
cause, and propose a fix. Do not roll back or change
|
|
488
|
-
explicit approval.
|
|
499
|
+
- Exit 1 / failed: read the findings and exceptions (`get_exception`),
|
|
500
|
+
investigate the likely cause, and propose a fix. Do not roll back or change
|
|
501
|
+
production without explicit approval.
|
|
489
502
|
- Exit 3: still verifying; run the command again.
|
|
490
|
-
- Not cleared yet
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
anything: the release can't clear until DeployAngel has a day of history.
|
|
503
|
+
- Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
|
|
504
|
+
If its status is "exercisable" or "waiting_for_activity", run
|
|
505
|
+
`bundle exec deployangel exercise --url=<production URL>`: it sends the
|
|
506
|
+
plan's read-only requests and records them on the release. Offer to
|
|
507
|
+
exercise what it skips: routes that change data only with a test account
|
|
508
|
+
or after asking. If the status is "warm_up", nothing run can clear it.
|
|
497
509
|
```
|
|
498
510
|
|
|
499
511
|
## Development
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -11,13 +11,14 @@ require_relative "client"
|
|
|
11
11
|
require_relative "ci_environment"
|
|
12
12
|
require_relative "verification_waiter"
|
|
13
13
|
require_relative "cli/formatter"
|
|
14
|
+
require_relative "exerciser"
|
|
14
15
|
|
|
15
16
|
module DeployAngel
|
|
16
17
|
# `deployangel` command for developers, CI, and coding agents.
|
|
17
18
|
# Runs without booting Rails.
|
|
18
19
|
class CLI
|
|
19
20
|
USAGE_ERROR = 5
|
|
20
|
-
COMMANDS = %w[release verify status exception check install mcp version help].freeze
|
|
21
|
+
COMMANDS = %w[release verify status plan exercise exception check install mcp version help].freeze
|
|
21
22
|
|
|
22
23
|
HELP = <<~TEXT
|
|
23
24
|
Usage: deployangel <command> [options]
|
|
@@ -30,6 +31,8 @@ module DeployAngel
|
|
|
30
31
|
status Latest deployment and its verification
|
|
31
32
|
plan What to exercise so a release [--commit=SHA | --version=V | --deployment=ID]
|
|
32
33
|
clears sooner [--format=text|json]
|
|
34
|
+
exercise Send the plan's read-only --url=PRODUCTION_URL [--commit=SHA | --version=V | --deployment=ID]
|
|
35
|
+
requests to production [--max-requests=200] [--dry-run]
|
|
33
36
|
exception Details for a fingerprint deployangel exception FINGERPRINT
|
|
34
37
|
check Report a smoke test result --name=NAME --status=pass|fail [--covers=a,b]
|
|
35
38
|
[--commit=SHA | --deployment=ID] [--details-url=URL]
|
|
@@ -98,10 +101,11 @@ module DeployAngel
|
|
|
98
101
|
production without explicit approval.
|
|
99
102
|
- Exit 3: still verifying; run the command again.
|
|
100
103
|
- Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
|
|
101
|
-
If its status is "exercisable" or "waiting_for_activity",
|
|
102
|
-
|
|
103
|
-
only
|
|
104
|
-
|
|
104
|
+
If its status is "exercisable" or "waiting_for_activity", run
|
|
105
|
+
`bundle exec deployangel exercise --url=<production URL>`: it sends the
|
|
106
|
+
plan's read-only requests and records them on the release. Offer to
|
|
107
|
+
exercise what it skips: routes that change data only with a test account
|
|
108
|
+
or after asking. If the status is "warm_up", nothing run can clear it.
|
|
105
109
|
#{AGENTS_END}
|
|
106
110
|
MARKDOWN
|
|
107
111
|
|
|
@@ -119,7 +123,7 @@ module DeployAngel
|
|
|
119
123
|
DOCKERFILE
|
|
120
124
|
|
|
121
125
|
def initialize(argv, env: ENV, stdin: $stdin, stdout: $stdout, stderr: $stderr, client: nil,
|
|
122
|
-
sleeper: ->(seconds) { sleep(seconds) }, clock: -> { Time.now }, git_head: nil, root: Dir.pwd)
|
|
126
|
+
sleeper: ->(seconds) { sleep(seconds) }, clock: -> { Time.now }, git_head: nil, root: Dir.pwd, requester: nil)
|
|
123
127
|
@root = root
|
|
124
128
|
@argv = argv.dup
|
|
125
129
|
@env = env
|
|
@@ -130,6 +134,7 @@ module DeployAngel
|
|
|
130
134
|
@sleeper = sleeper
|
|
131
135
|
@clock = clock
|
|
132
136
|
@git_head = git_head
|
|
137
|
+
@requester = requester
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
def run
|
|
@@ -139,6 +144,7 @@ module DeployAngel
|
|
|
139
144
|
when "verify" then verify
|
|
140
145
|
when "status" then verify(status: true)
|
|
141
146
|
when "plan" then plan
|
|
147
|
+
when "exercise" then exercise
|
|
142
148
|
when "exception" then exception
|
|
143
149
|
when "check" then check
|
|
144
150
|
when "install" then install
|
|
@@ -238,6 +244,56 @@ module DeployAngel
|
|
|
238
244
|
0
|
|
239
245
|
end
|
|
240
246
|
|
|
247
|
+
# Sends the plan's read-only requests from here, then records what it
|
|
248
|
+
# sent on the release (spec §16, Exercise records).
|
|
249
|
+
def exercise
|
|
250
|
+
options = parse(max_requests: Exerciser::MAX_REQUESTS, dry_run: false) do |o, opts|
|
|
251
|
+
o.on("--url=URL") { |v| opts[:url] = v }
|
|
252
|
+
o.on("--commit=SHA") { |v| opts[:commit] = v }
|
|
253
|
+
o.on("--version=VERSION") { |v| opts[:version] = v }
|
|
254
|
+
o.on("--deployment=ID") { |v| opts[:deployment_id] = v }
|
|
255
|
+
o.on("--max-requests=N", Integer) { |v| opts[:max_requests] = v }
|
|
256
|
+
o.on("--dry-run") { opts[:dry_run] = true }
|
|
257
|
+
end
|
|
258
|
+
return usage_error("exercise needs --url, the app's production URL, like https://example.com") unless options[:url].to_s.match?(%r{\Ahttps?://[^/\s]+})
|
|
259
|
+
|
|
260
|
+
target = target_from(options)
|
|
261
|
+
return usage_error("no target: pass --commit, --version, or --deployment, or run inside a git repository") unless target
|
|
262
|
+
|
|
263
|
+
outcome = VerificationWaiter.new(client: client, sleeper: @sleeper, clock: @clock).wait(target, wait: false)
|
|
264
|
+
if outcome.not_found
|
|
265
|
+
@stderr.puts("deployangel: no deployment found for #{target.values.first}")
|
|
266
|
+
return outcome.exit_code
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
document = outcome.document
|
|
270
|
+
plan = document["exercise_plan"] || {}
|
|
271
|
+
unless %w[exercisable waiting_for_activity].include?(plan["status"])
|
|
272
|
+
@stdout.puts("Nothing to exercise. #{plan["summary"]}".rstrip)
|
|
273
|
+
return 0
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
exerciser = Exerciser.new(base_url: options[:url], max_requests: options[:max_requests].clamp(1, Exerciser::MAX_REQUESTS),
|
|
277
|
+
sleeper: @sleeper, requester: @requester)
|
|
278
|
+
if options[:dry_run]
|
|
279
|
+
targets, skipped = exerciser.plan(plan)
|
|
280
|
+
@stdout.puts("Would send #{targets.sum(&:count)} requests:", *targets.map { |t| " #{exerciser.url_for(t.path)} ×#{t.count}" })
|
|
281
|
+
@stdout.puts("Skipped:", *skipped.map { |s| " #{s["key"]} (#{s["reason"]})" }) if skipped.any?
|
|
282
|
+
return 0
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
result = exerciser.run(plan)
|
|
286
|
+
@stdout.puts("Sent #{result.sent} requests to #{options[:url]}:",
|
|
287
|
+
*result.routes.map { |r| " #{r["key"]} ×#{r["requests"]}: #{r["statuses"].map { |kind, n| "#{n} #{kind}" }.join(", ")}" })
|
|
288
|
+
@stdout.puts("Skipped:", *result.skipped.map { |s| " #{s["key"]} (#{s["reason"]})" }) if result.skipped.any?
|
|
289
|
+
@stdout.puts("Stopped early: #{options[:url]} isn't answering.") if result.unreachable
|
|
290
|
+
if result.routes.any?
|
|
291
|
+
client.record_exercise(document.dig("deployment", "id"), routes: result.routes, skipped: result.skipped, ran_at: @clock.call.utc.iso8601)
|
|
292
|
+
@stdout.puts("Recorded on the release. Wait for the verdict with: deployangel verify --wait")
|
|
293
|
+
end
|
|
294
|
+
result.unreachable ? USAGE_ERROR : 0
|
|
295
|
+
end
|
|
296
|
+
|
|
241
297
|
def exception
|
|
242
298
|
fingerprint = @argv.shift or return usage_error("exception needs a FINGERPRINT")
|
|
243
299
|
format = parse(format: nil) { |o, opts| o.on("--format=FORMAT", %w[text json]) { |v| opts[:format] = v } }[:format]
|
data/lib/deployangel/client.rb
CHANGED
|
@@ -49,6 +49,12 @@ module DeployAngel
|
|
|
49
49
|
{ name: name, status: status, covers: covers, details_url: details_url }.compact)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# What `deployangel exercise` sent (spec §16, Exercise records). A
|
|
53
|
+
# "CLI & coding agents" token may record one: it only labels the release.
|
|
54
|
+
def record_exercise(deployment_id, routes:, skipped:, ran_at:)
|
|
55
|
+
post("/api/v1/deployments/#{deployment_id}/exercises", { routes: routes, skipped: skipped, ran_at: ran_at })
|
|
56
|
+
end
|
|
57
|
+
|
|
52
58
|
private
|
|
53
59
|
def get(path, params = {})
|
|
54
60
|
uri = URI("#{@endpoint}#{path}")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module DeployAngel
|
|
7
|
+
# `deployangel exercise`: sends a release's exercise plan's read-only
|
|
8
|
+
# requests to production from the customer's side, then records what it
|
|
9
|
+
# sent, so the release page says which routes were exercised that way.
|
|
10
|
+
# Only GET routes with no path parameters and not marked as changing data;
|
|
11
|
+
# the rest are skipped and named, for the agent or a smoke test with a test
|
|
12
|
+
# account. The requests count like any traffic through the app's agent.
|
|
13
|
+
class Exerciser
|
|
14
|
+
MAX_REQUESTS = 200
|
|
15
|
+
INTERVAL = 0.2 # about 5 requests a second
|
|
16
|
+
TIMEOUT = 10
|
|
17
|
+
# Stop when the app isn't answering, rather than send the rest into it.
|
|
18
|
+
MAX_CONSECUTIVE_ERRORS = 5
|
|
19
|
+
# A route table can list pages an app doesn't serve, like the edit page
|
|
20
|
+
# of a resource that has none. One answer like this is enough to know.
|
|
21
|
+
NOT_SERVED = [ 404, 405, 410 ].freeze
|
|
22
|
+
# Rails' :id and *path, Django's <int:pk>, FastAPI's {id}.
|
|
23
|
+
PARAMETER = /\/[:*{<]/
|
|
24
|
+
|
|
25
|
+
Target = Struct.new(:key, :path, :count)
|
|
26
|
+
Result = Struct.new(:routes, :skipped, :sent, :unreachable, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
def initialize(base_url:, max_requests: MAX_REQUESTS, sleeper: ->(seconds) { sleep(seconds) }, requester: nil)
|
|
29
|
+
@base = URI(base_url)
|
|
30
|
+
@max_requests = max_requests
|
|
31
|
+
@sleeper = sleeper
|
|
32
|
+
@requester = requester || method(:get)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# What would be sent: [targets, skipped].
|
|
36
|
+
def plan(exercise_plan)
|
|
37
|
+
skipped = []
|
|
38
|
+
targets = []
|
|
39
|
+
Array(exercise_plan["items"]).each do |item|
|
|
40
|
+
next unless item["kind"] == "route"
|
|
41
|
+
|
|
42
|
+
method, path = item["key"].to_s.split(" ", 2)
|
|
43
|
+
reason =
|
|
44
|
+
if item["mutating"] || method != "GET" then "changes data"
|
|
45
|
+
elsif path.nil? || path.match?(PARAMETER) then "needs a path parameter"
|
|
46
|
+
end
|
|
47
|
+
next skipped << { "key" => item["key"], "reason" => reason } if reason
|
|
48
|
+
|
|
49
|
+
needed = item["runs_needed"] ? item["runs_needed"].to_i - item["runs"].to_i : 1
|
|
50
|
+
targets << Target.new(item["key"], path, [ needed, 1 ].max)
|
|
51
|
+
end
|
|
52
|
+
spread_shortfall(targets, exercise_plan.dig("shortfall", "requests"))
|
|
53
|
+
cap(targets)
|
|
54
|
+
[ targets, skipped ]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A route whose first request says it isn't served gets no more; its
|
|
58
|
+
# share goes to the routes that answered.
|
|
59
|
+
def run(exercise_plan)
|
|
60
|
+
targets, skipped = plan(exercise_plan)
|
|
61
|
+
@statuses = Hash.new { |hash, key| hash[key] = Hash.new(0) }
|
|
62
|
+
@sent = 0
|
|
63
|
+
@errors_in_a_row = 0
|
|
64
|
+
leftover = 0
|
|
65
|
+
answered = []
|
|
66
|
+
targets.each do |target|
|
|
67
|
+
target.count.times do |i|
|
|
68
|
+
break if unreachable?
|
|
69
|
+
|
|
70
|
+
status = send_request(target)
|
|
71
|
+
if i.zero? && NOT_SERVED.include?(status)
|
|
72
|
+
leftover += target.count - 1
|
|
73
|
+
break
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
answered << target if @statuses[target.key].keys.intersect?(%w[2xx 3xx])
|
|
77
|
+
end
|
|
78
|
+
leftover.times { |i| send_request(answered[i % answered.size]) unless unreachable? } if answered.any?
|
|
79
|
+
|
|
80
|
+
routes = targets.filter_map do |target|
|
|
81
|
+
statuses = @statuses.fetch(target.key, nil) or next
|
|
82
|
+
{ "key" => target.key, "requests" => statuses.values.sum, "statuses" => statuses.to_h }
|
|
83
|
+
end
|
|
84
|
+
Result.new(routes: routes, skipped: skipped, sent: @sent, unreachable: unreachable?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def url_for(path)
|
|
88
|
+
uri = @base.dup
|
|
89
|
+
uri.path = "#{@base.path.chomp("/")}#{path}"
|
|
90
|
+
uri.query = nil
|
|
91
|
+
uri.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
def send_request(target)
|
|
96
|
+
@sleeper.call(INTERVAL) if @sent.positive?
|
|
97
|
+
status = @requester.call(url_for(target.path))
|
|
98
|
+
@sent += 1
|
|
99
|
+
kind = status ? "#{status / 100}xx" : "error"
|
|
100
|
+
@statuses[target.key][kind] += 1
|
|
101
|
+
@errors_in_a_row = kind == "error" ? @errors_in_a_row + 1 : 0
|
|
102
|
+
status
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unreachable?
|
|
106
|
+
@errors_in_a_row >= MAX_CONSECUTIVE_ERRORS
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Requests the release is short of overall, spread across the routes,
|
|
110
|
+
# or sent to the home page when the plan names no route to send them to.
|
|
111
|
+
def spread_shortfall(targets, requests)
|
|
112
|
+
return unless requests
|
|
113
|
+
|
|
114
|
+
extra = requests["need"].to_i - requests["have"].to_i - targets.sum(&:count)
|
|
115
|
+
return unless extra.positive?
|
|
116
|
+
|
|
117
|
+
targets << Target.new("GET /", "/", 0) if targets.empty?
|
|
118
|
+
extra.times { |i| targets[i % targets.size].count += 1 }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cap(targets)
|
|
122
|
+
targets.max_by(&:count).count -= 1 while targets.sum(&:count) > @max_requests
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The status code, or nil when there was no answer. Redirects aren't
|
|
126
|
+
# followed: the request already reached the app.
|
|
127
|
+
def get(url)
|
|
128
|
+
uri = URI(url)
|
|
129
|
+
request = Net::HTTP::Get.new(uri)
|
|
130
|
+
request["User-Agent"] = "DeployAngel-Exercise/#{DeployAngel::VERSION} (+https://www.deployangel.com/docs#exercise)"
|
|
131
|
+
request["Accept"] = "text/html,application/json;q=0.9,*/*;q=0.8"
|
|
132
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
|
|
133
|
+
http.request(request).code.to_i
|
|
134
|
+
end
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/deployangel/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deployangel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Owens
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/deployangel/core/redaction.rb
|
|
47
47
|
- lib/deployangel/core/release.rb
|
|
48
48
|
- lib/deployangel/core/transport.rb
|
|
49
|
+
- lib/deployangel/exerciser.rb
|
|
49
50
|
- lib/deployangel/fork_hook.rb
|
|
50
51
|
- lib/deployangel/mcp.rb
|
|
51
52
|
- lib/deployangel/metadata.rb
|