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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb68d7ed3170cb16932b6ab0de9304c4388e9df001067cf6c0b6678e92fae0d1
4
- data.tar.gz: dc867b57741dc75e868284fd8a1f30450d756306fcb891f66331538dacf3486f
3
+ metadata.gz: 275f765f13eee93ddc36007727d1f2278b343a524562c062eeab1a258f8abb16
4
+ data.tar.gz: f167ad720f710b5cf3a527632795972a5dbbe972309bbbf470a937425d0282e7
5
5
  SHA512:
6
- metadata.gz: 8427f099ce309a80180e0dbb3033e465e4d82c8ad6f0f72c803623594feb6b9a831a16e7ec25c21e866c792d0acfa50d39f453eb17b2d282f32bd765f8133be4
7
- data.tar.gz: 4c19dc7ec0a6730724a466dc51b91f99bddf073b9dc3af480a584ee62417a7637543f64f8f9d4784be176416067162ad79200031e22ed180eb30c70da7ec7072
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. A "CLI & coding agents" token can run
607
- it. `--dry-run` shows what it would send.
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. Do not roll back or change
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
 
@@ -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. Do not roll back or change
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["statuses"].map { |kind, n| "#{n} #{kind}" }.join(", ")}" })
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
- Result = Struct.new(:routes, :skipped, :sent, :unreachable, keyword_init: true)
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
@@ -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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.33"
4
+ VERSION = "0.1.35"
5
5
  end
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.33
4
+ version: 0.1.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens