deployangel 0.1.33 → 0.1.35
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 +15 -0
- data/README.md +6 -3
- data/lib/deployangel/cli/formatter.rb +4 -1
- data/lib/deployangel/cli.rb +17 -2
- data/lib/deployangel/exerciser.rb +17 -2
- data/lib/deployangel/mcp.rb +2 -1
- data/lib/deployangel/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 275f765f13eee93ddc36007727d1f2278b343a524562c062eeab1a258f8abb16
|
|
4
|
+
data.tar.gz: f167ad720f710b5cf3a527632795972a5dbbe972309bbbf470a937425d0282e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdd30481a4d28645ef05f5d570f253d73029258581743045677e5a7e1c19a395b28082656aa44f588eac2c8be074b01a93240102143a822d166fa250325efd93
|
|
7
|
+
data.tar.gz: 5190303791d16fb83affbed23cd01e5b20c1b28a245f94824ee9f01670ec454a9b6c9696d632cabc3ce7c0990da8d0809adb14b7c3eddeda7aa8a5ec077f7f54
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.35 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
- `deployangel exercise` prints each route's status codes (`404 ×1`)
|
|
6
|
+
instead of their class (`1 4xx`). When every page answers 404, 405, or
|
|
7
|
+
410, as when `--url` is the app's marketing site rather than its own
|
|
8
|
+
host, it says so, records nothing on the release, and exits 5.
|
|
9
|
+
|
|
10
|
+
## 0.1.34 (2026-11-01)
|
|
11
|
+
|
|
12
|
+
- `deployangel verify` prints a failed release's next steps: the last
|
|
13
|
+
cleared release a rollback would go to, with the migrations it would
|
|
14
|
+
cross, and where to look first. They come from the verdict document's new
|
|
15
|
+
`next_steps` field, so servers before 2026-11-01 show none. The coding
|
|
16
|
+
agent instructions mention them, and still say to ask before rolling back.
|
|
17
|
+
|
|
3
18
|
## 0.1.33 (2026-10-28)
|
|
4
19
|
|
|
5
20
|
- An exception whose message has bytes that aren't valid UTF-8, as a
|
data/README.md
CHANGED
|
@@ -603,8 +603,10 @@ requests, about 5 a second, as `DeployAngel-Exercise`), then records what it
|
|
|
603
603
|
sent on the release, where the page lists it under "Exercised from your
|
|
604
604
|
side". It skips and names routes that change data or need a path
|
|
605
605
|
parameter, and stops requesting a page after its first 404, sending its
|
|
606
|
-
share to the pages that answered.
|
|
607
|
-
|
|
606
|
+
share to the pages that answered. It prints each route's status codes. If
|
|
607
|
+
every page answers 404, 405, or 410, it records nothing and exits 5, since
|
|
608
|
+
the URL is probably not the app's own host (a marketing site, say). A "CLI
|
|
609
|
+
& coding agents" token can run it. `--dry-run` shows what it would send.
|
|
608
610
|
|
|
609
611
|
### Suggested instructions for your coding agent
|
|
610
612
|
|
|
@@ -629,7 +631,8 @@ verdict: call the `wait_for_verification` MCP tool with the commit and
|
|
|
629
631
|
The release is NOT cleared.
|
|
630
632
|
- Exit 2 / inconclusive: the release is NOT verified. Do not claim success.
|
|
631
633
|
- Exit 1 / failed: read the findings and exceptions (`get_exception`),
|
|
632
|
-
investigate the likely cause, and propose a fix.
|
|
634
|
+
investigate the likely cause, and propose a fix. `next_steps` names the last
|
|
635
|
+
cleared release a rollback would go to. Do not roll back or change
|
|
633
636
|
production without explicit approval.
|
|
634
637
|
- Exit 3: still verifying; run the command again.
|
|
635
638
|
- Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
|
|
@@ -110,7 +110,10 @@ module DeployAngel
|
|
|
110
110
|
end ],
|
|
111
111
|
[ "Changed but not yet exercised", changed.map { |item| item["key"] } ],
|
|
112
112
|
[ "To clear sooner, exercise (deployangel plan for details)", exercisable_items(document).first(5) ],
|
|
113
|
-
[ "Late regressions", Array(document["late_regressions"]).map { |late| late["summary"] } ]
|
|
113
|
+
[ "Late regressions", Array(document["late_regressions"]).map { |late| late["summary"] } ],
|
|
114
|
+
# A failed release's rollback target and where to look first
|
|
115
|
+
# (servers from 2026-11-01 send them).
|
|
116
|
+
[ "Next steps", Array(document["next_steps"]).map { |step| step["text"] } ]
|
|
114
117
|
].reject { |_, items| items.empty? }
|
|
115
118
|
end
|
|
116
119
|
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -108,7 +108,8 @@ module DeployAngel
|
|
|
108
108
|
The release is NOT cleared.
|
|
109
109
|
- Exit 2 / inconclusive: the release is NOT verified. Do not claim success.
|
|
110
110
|
- Exit 1 / failed: read the findings and exceptions (`get_exception`),
|
|
111
|
-
investigate the likely cause, and propose a fix.
|
|
111
|
+
investigate the likely cause, and propose a fix. `next_steps` names the last
|
|
112
|
+
cleared release a rollback would go to. Do not roll back or change
|
|
112
113
|
production without explicit approval.
|
|
113
114
|
- Exit 3: still verifying; run the command again.
|
|
114
115
|
- Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
|
|
@@ -318,9 +319,18 @@ module DeployAngel
|
|
|
318
319
|
|
|
319
320
|
result = exerciser.run(plan)
|
|
320
321
|
@stdout.puts("Sent #{result.sent} requests to #{options[:url]}:",
|
|
321
|
-
*result.routes.map { |r| " #{r["key"]} ×#{r["requests"]}: #{r["
|
|
322
|
+
*result.routes.map { |r| " #{r["key"]} ×#{r["requests"]}: #{status_codes(result.codes[r["key"]])}" })
|
|
322
323
|
@stdout.puts("Skipped:", *result.skipped.map { |s| " #{s["key"]} (#{s["reason"]})" }) if result.skipped.any?
|
|
323
324
|
@stdout.puts("Stopped early: #{options[:url]} isn't answering.") if result.unreachable
|
|
325
|
+
# Requests that reached no page aren't worth recording: the app
|
|
326
|
+
# likely never saw them.
|
|
327
|
+
if result.routes.any? && result.nothing_served? && !result.unreachable
|
|
328
|
+
@stdout.puts("", "No page answered at #{options[:url]}: every route said it doesn't exist. Is that the app's own host, " \
|
|
329
|
+
"and not a marketing site or a different domain? Pass the URL where the app's pages load.")
|
|
330
|
+
@stdout.puts("#{result.unsent} more requests weren't sent, since no route answered.") if result.unsent.positive?
|
|
331
|
+
@stdout.puts("Nothing was recorded on the release.")
|
|
332
|
+
return USAGE_ERROR
|
|
333
|
+
end
|
|
324
334
|
if result.routes.any?
|
|
325
335
|
client.record_exercise(document.dig("deployment", "id"), routes: result.routes, skipped: result.skipped, ran_at: @clock.call.utc.iso8601)
|
|
326
336
|
@stdout.puts("Recorded on the release. Wait for the verdict with: deployangel verify --wait")
|
|
@@ -328,6 +338,11 @@ module DeployAngel
|
|
|
328
338
|
result.unreachable ? USAGE_ERROR : 0
|
|
329
339
|
end
|
|
330
340
|
|
|
341
|
+
# "200 ×4, 500 ×1", in the order they came.
|
|
342
|
+
def status_codes(codes)
|
|
343
|
+
codes.map { |code, n| "#{code || "no answer"} ×#{n}" }.join(", ")
|
|
344
|
+
end
|
|
345
|
+
|
|
331
346
|
def exception
|
|
332
347
|
fingerprint = @argv.shift or return usage_error("exception needs a FINGERPRINT")
|
|
333
348
|
format = parse(format: nil) { |o, opts| o.on("--format=FORMAT", %w[text json]) { |v| opts[:format] = v } }[:format]
|
|
@@ -23,7 +23,18 @@ module DeployAngel
|
|
|
23
23
|
PARAMETER = /\/[:*{<]/
|
|
24
24
|
|
|
25
25
|
Target = Struct.new(:key, :path, :count)
|
|
26
|
-
|
|
26
|
+
# codes: each route's exact status codes, nil for no answer, for
|
|
27
|
+
# display; routes carries status classes, which is what's recorded.
|
|
28
|
+
# unsent: requests the plan wanted that went nowhere, since no route
|
|
29
|
+
# answered.
|
|
30
|
+
Result = Struct.new(:routes, :skipped, :sent, :unreachable, :codes, :unsent, keyword_init: true) do
|
|
31
|
+
# Every answer said the page isn't served. Usually the wrong host,
|
|
32
|
+
# like the app's marketing site.
|
|
33
|
+
def nothing_served?
|
|
34
|
+
answers = codes.values.flat_map(&:keys)
|
|
35
|
+
answers.any? && answers.all? { |code| NOT_SERVED.include?(code) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
27
38
|
|
|
28
39
|
def initialize(base_url:, max_requests: MAX_REQUESTS, sleeper: ->(seconds) { sleep(seconds) }, requester: nil)
|
|
29
40
|
@base = URI(base_url)
|
|
@@ -59,6 +70,7 @@ module DeployAngel
|
|
|
59
70
|
def run(exercise_plan)
|
|
60
71
|
targets, skipped = plan(exercise_plan)
|
|
61
72
|
@statuses = Hash.new { |hash, key| hash[key] = Hash.new(0) }
|
|
73
|
+
@codes = Hash.new { |hash, key| hash[key] = Hash.new(0) }
|
|
62
74
|
@sent = 0
|
|
63
75
|
@errors_in_a_row = 0
|
|
64
76
|
leftover = 0
|
|
@@ -76,12 +88,14 @@ module DeployAngel
|
|
|
76
88
|
answered << target if @statuses[target.key].keys.intersect?(%w[2xx 3xx])
|
|
77
89
|
end
|
|
78
90
|
leftover.times { |i| send_request(answered[i % answered.size]) unless unreachable? } if answered.any?
|
|
91
|
+
unsent = answered.empty? ? leftover : 0
|
|
79
92
|
|
|
80
93
|
routes = targets.filter_map do |target|
|
|
81
94
|
statuses = @statuses.fetch(target.key, nil) or next
|
|
82
95
|
{ "key" => target.key, "requests" => statuses.values.sum, "statuses" => statuses.to_h }
|
|
83
96
|
end
|
|
84
|
-
Result.new(routes: routes, skipped: skipped, sent: @sent, unreachable: unreachable
|
|
97
|
+
Result.new(routes: routes, skipped: skipped, sent: @sent, unreachable: unreachable?,
|
|
98
|
+
codes: routes.to_h { |route| [ route["key"], @codes[route["key"]].to_h ] }, unsent: unsent)
|
|
85
99
|
end
|
|
86
100
|
|
|
87
101
|
def url_for(path)
|
|
@@ -98,6 +112,7 @@ module DeployAngel
|
|
|
98
112
|
@sent += 1
|
|
99
113
|
kind = status ? "#{status / 100}xx" : "error"
|
|
100
114
|
@statuses[target.key][kind] += 1
|
|
115
|
+
@codes[target.key][status] += 1
|
|
101
116
|
@errors_in_a_row = kind == "error" ? @errors_in_a_row + 1 : 0
|
|
102
117
|
status
|
|
103
118
|
end
|
data/lib/deployangel/mcp.rb
CHANGED
|
@@ -18,7 +18,8 @@ module DeployAngel
|
|
|
18
18
|
DeployAngel verifies deployments in production. After deploying, call wait_for_verification
|
|
19
19
|
with until "initial" (about 15 minutes) or "verdict". Verdicts: verified means cleared, so you may
|
|
20
20
|
report the release as successful. inconclusive means NOT verified: never report it as success.
|
|
21
|
-
failed means production regressed: read the findings and exceptions and investigate.
|
|
21
|
+
failed means production regressed: read the findings and exceptions and investigate. next_steps
|
|
22
|
+
names the last cleared release a rollback would go to and the migrations it would cross.
|
|
22
23
|
Confidence medium clears a release just as high does (high also needs 80% coverage and a full
|
|
23
24
|
baseline): never call a medium clearance weak. suspicious true means warnings DeployAngel is
|
|
24
25
|
watching closely, not a failure: report them as "watching closely:" and the watching_closely items,
|
data/lib/deployangel/version.rb
CHANGED