deployangel 0.1.34 → 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: 7e0552271dfa638f21feb4c8591729a8c226df32dc75201a12a8e41843534f75
4
- data.tar.gz: d62f917c014872a66458dab5e73de67b87b594b0e654b519388d193cd62d4965
3
+ metadata.gz: 275f765f13eee93ddc36007727d1f2278b343a524562c062eeab1a258f8abb16
4
+ data.tar.gz: f167ad720f710b5cf3a527632795972a5dbbe972309bbbf470a937425d0282e7
5
5
  SHA512:
6
- metadata.gz: 9e116aca53991800ed66a98f288fad4706f51baf45b174678156cfcd51202bac16e7d151ef5c323ad97d14f6ca774bf2092f034ef2764fd2106d530f2a48c0ec
7
- data.tar.gz: 2f31abc979d9de0d0fac5da35d975025240144df25bc82693c25c5ca271ecb206032437cbfbca61571a78a70559f40b5083e423d98d94f1a35290968fa5b01bd
6
+ metadata.gz: cdd30481a4d28645ef05f5d570f253d73029258581743045677e5a7e1c19a395b28082656aa44f588eac2c8be074b01a93240102143a822d166fa250325efd93
7
+ data.tar.gz: 5190303791d16fb83affbed23cd01e5b20c1b28a245f94824ee9f01670ec454a9b6c9696d632cabc3ce7c0990da8d0809adb14b7c3eddeda7aa8a5ec077f7f54
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.1.34 (2026-11-01)
4
11
 
5
12
  - `deployangel verify` prints a failed release's next steps: the last
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
 
@@ -319,9 +319,18 @@ module DeployAngel
319
319
 
320
320
  result = exerciser.run(plan)
321
321
  @stdout.puts("Sent #{result.sent} requests to #{options[:url]}:",
322
- *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"]])}" })
323
323
  @stdout.puts("Skipped:", *result.skipped.map { |s| " #{s["key"]} (#{s["reason"]})" }) if result.skipped.any?
324
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
325
334
  if result.routes.any?
326
335
  client.record_exercise(document.dig("deployment", "id"), routes: result.routes, skipped: result.skipped, ran_at: @clock.call.utc.iso8601)
327
336
  @stdout.puts("Recorded on the release. Wait for the verdict with: deployangel verify --wait")
@@ -329,6 +338,11 @@ module DeployAngel
329
338
  result.unreachable ? USAGE_ERROR : 0
330
339
  end
331
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
+
332
346
  def exception
333
347
  fingerprint = @argv.shift or return usage_error("exception needs a FINGERPRINT")
334
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.34"
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.34
4
+ version: 0.1.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens