deployangel 0.1.36 → 0.1.38

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: eb03031f58bd01feb61e1f478f4108eb3c969527e28cf86d970355a6bede0031
4
- data.tar.gz: 3dd9679d37e80b23e449ee99ffd29bd3db9700bf8e2b5bfa665bf4413e2e4fb7
3
+ metadata.gz: 9a0944295d3cbddf9d6ff41ce21ad608b3c9615dc2910bfed6ef9745ffde6e93
4
+ data.tar.gz: 7197967f339bb1af5057528e2b08b9e894379051401d059f02a91ec6cd3083ad
5
5
  SHA512:
6
- metadata.gz: bbaff8f03fce84081cdf13786dac7d26912ffeb9eae1c3f032f91d3b00afd23ba95a076c6213846cd44390017e8b7f8cdfd7b63833a07fcef9f782b4e9576dc0
7
- data.tar.gz: 2919b83e169d1ac91175c733d9366df8e05b4910f8722a99cb3fdae57a07dda205be55dce418f2c0c7e1d8e382dc17a9007e23c96126b0174d24ed24588c13be
6
+ metadata.gz: 9fc73a15daea7553d4a04121c001a49feef7a15195b0313b1115ab572a387cc053bf80196d6c8d79baee8c980df2833c198cdf7a2e0dcdeda18d81235dd3c208
7
+ data.tar.gz: 4d5fe66f67476bd5c0e41137d6f3f38746d31da776ea295526e4becd463da21f61bf4d23a5d0ebfaac61df4c84d0d4692b67e49797d5e05a6cb63fa5e59a706f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.38 (2026-11-04)
4
+
5
+ - `deployangel verify` shows a volume drop (servers from 2026-11-04) as
6
+ what ran against what was expected (`0 requests, about 27 expected`),
7
+ and lists findings a person accepted as an intended change, with who
8
+ and why. A failing finding that could be intended gets the link where a
9
+ person can accept it; the CLI and MCP server can't, so an agent never
10
+ accepts its own failure.
11
+ - The MCP server's instructions, and the block `deployangel install
12
+ agents` writes, tell agents to hand that link to the user with their
13
+ evidence and never report the release as a success.
14
+
15
+ ## 0.1.37 (2026-11-03)
16
+
17
+ - An exception raised entirely in gems, with no frame from the app's
18
+ code, now sends its first library frames as its stack trace
19
+ (`net-smtp/lib/net/smtp.rb#start`), normalized like the top frame, with
20
+ no absolute paths or versions. Before, it sent none, so an error such as
21
+ a mailer job's `Net::ReadTimeout` showed no trace.
22
+
3
23
  ## 0.1.36 (2026-11-01)
4
24
 
5
25
  - `deployangel exercise --param id=42` fills path parameters (`:id`,
data/README.md CHANGED
@@ -149,7 +149,8 @@ the dashboard says so.
149
149
  - Release identity, runtime versions, and a per-process instance ID.
150
150
 
151
151
  - Exceptions: a stable fingerprint, the exception class, the first line of the
152
- message, and application frames only. In the message, numbers, IDs, emails,
152
+ message, and application frames only, or the first library frames (gem
153
+ paths without versions) when none is the app's. In the message, numbers, IDs, emails,
153
154
  UUIDs, long hex strings, and quoted values are replaced with placeholders, and
154
155
  so are the request's host and, with ros-apartment, the tenants the request or
155
156
  job switched to. Other words are kept: `Payment failed for Jane Doe` is sent
@@ -238,7 +239,7 @@ end
238
239
  ### Exception messages
239
240
 
240
241
  To send exceptions without any message, only their class, fingerprint, and
241
- application frames:
242
+ stack frames:
242
243
 
243
244
  ```ruby
244
245
  DeployAngel.configure do |config|
@@ -10,7 +10,7 @@ module DeployAngel
10
10
  "job_failure_rate" => "Job failure rate", "job_duration" => "Job duration p95", "queue_latency" => "Queue latency p95",
11
11
  "new_fingerprint" => "New exception", "fingerprint_amplification" => "Exception amplification",
12
12
  "missing_recurring_job" => "Recurring job", "first_use_failure" => "First-use failure", "external_check" => "External check",
13
- "checkpoint_rate" => "Checkpoint"
13
+ "checkpoint_rate" => "Checkpoint", "volume_drop" => "Volume"
14
14
  }.freeze
15
15
  DURATIONS = %w[p95_latency job_duration queue_latency].freeze
16
16
  # Exercise plan statuses with something worth running now.
@@ -18,6 +18,9 @@ module DeployAngel
18
18
  REASONS = { "normally_active" => "normally active", "changed_in_release" => "changed in this release",
19
19
  "rarely_used" => "rarely used", "critical_flow" => "critical flow" }.freeze
20
20
  COUNTS = %w[new_fingerprint checkpoint_rate].freeze
21
+ # Findings worth listing: problems, and changes a person accepted as
22
+ # intended (servers from 2026-11-04 send "accepted").
23
+ LISTED = %w[failing warning accepted].freeze
21
24
 
22
25
  module_function
23
26
 
@@ -26,7 +29,10 @@ module DeployAngel
26
29
  problems = problems(document)
27
30
  if problems.any?
28
31
  lines << "Findings:"
29
- problems.each { |f| lines << " #{f["status"].ljust(8)} #{finding_line(f)}" }
32
+ problems.each do |f|
33
+ lines << " #{f["status"].ljust(8)} #{finding_line(f)}"
34
+ acceptance_lines(f).each { |line| lines << " #{line}" }
35
+ end
30
36
  end
31
37
  if (gathering = gathering_line(document))
32
38
  lines << "#{gathering} (--format=json for details)"
@@ -44,7 +50,7 @@ module DeployAngel
44
50
  problems = problems(document)
45
51
  if problems.any?
46
52
  lines.push("", "| Status | Finding |", "| --- | --- |")
47
- problems.each { |f| lines << "| #{f["status"]} | #{cell(finding_line(f))} |" }
53
+ problems.each { |f| lines << "| #{f["status"]} | #{cell([ finding_line(f), *acceptance_lines(f) ].join(". "))} |" }
48
54
  end
49
55
  if (gathering = gathering_line(document))
50
56
  lines.push("", gathering)
@@ -85,7 +91,19 @@ module DeployAngel
85
91
  end
86
92
 
87
93
  def problems(document)
88
- Array(document["findings"]).select { |f| %w[failing warning].include?(f["status"]) }
94
+ Array(document["findings"]).select { |f| LISTED.include?(f["status"]) }
95
+ end
96
+
97
+ # Who accepted a finding as an intended change, or where a person can.
98
+ # Only a person can, on the release page: the API can't.
99
+ def acceptance_lines(finding)
100
+ if (accepted = finding["accepted_as_intended"])
101
+ [ "Accepted as intended by #{accepted["by"]}: #{accepted["note"]}" ]
102
+ elsif finding["accept_url"]
103
+ [ "If this change was intended, a person can accept it: #{finding["accept_url"]}" ]
104
+ else
105
+ []
106
+ end
89
107
  end
90
108
 
91
109
  def gathering_line(document)
@@ -216,7 +234,12 @@ module DeployAngel
216
234
 
217
235
  def finding_line(finding)
218
236
  label = "#{LABELS.fetch(finding["signal"], finding["signal"])} on #{finding["scope"]}"
219
- if COUNTS.include?(finding["signal"])
237
+ if finding["signal"] == "volume_drop"
238
+ # Its baseline value is what the release before predicts for this
239
+ # much traffic.
240
+ unit = finding["scope"].to_s.start_with?("route:") ? "requests" : "runs"
241
+ "#{label}: #{finding["observed_value"].to_i} #{unit}, about #{finding["baseline_value"].to_f.round} expected"
242
+ elsif COUNTS.include?(finding["signal"])
220
243
  "#{label}: #{finding["observed_value"].to_i} occurrences"
221
244
  elsif finding["signal"] == "missing_recurring_job"
222
245
  # Its value is the job's interval in seconds, not a rate.
@@ -112,6 +112,10 @@ module DeployAngel
112
112
  investigate the likely cause, and propose a fix. `next_steps` names the last
113
113
  cleared release a rollback would go to. Do not roll back or change
114
114
  production without explicit approval.
115
+ - A failed release whose failing finding is a volume or checkpoint drop you
116
+ made on purpose (a route or job you removed): you can't accept it. Tell the
117
+ user why you think it was intended, with your evidence, and give them the
118
+ finding's `accept_url` so a person decides. It stays failed until then.
115
119
  - Exit 3: still verifying; run the command again.
116
120
  - Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
117
121
  If its status is "exercisable" or "waiting_for_activity", run
@@ -46,10 +46,14 @@ module DeployAngel
46
46
  "#{normalize_path(path, root)}##{label}"
47
47
  end
48
48
 
49
- # Application frames only, relative to the root.
49
+ # Application frames only, relative to the root. An exception raised
50
+ # entirely in gems (a mailer job's SMTP timeout) has none, so it gets its
51
+ # first frames instead, normalized as in top_frame: they're the only way
52
+ # to tell which gem's path failed.
50
53
  def backtrace(exception, root:)
51
- locations(exception).select { |path, _| app_path?(path, root) }.first(MAX_BACKTRACE)
52
- .map { |path, label| "#{normalize_path(path, root)}##{label}" }
54
+ frames = locations(exception)
55
+ app = frames.select { |path, _| app_path?(path, root) }
56
+ (app.empty? ? frames : app).first(MAX_BACKTRACE).map { |path, label| "#{normalize_path(path, root)}##{label}" }
53
57
  end
54
58
 
55
59
  # Only quoted values, numbers, emails, UUIDs, and long hex are replaced;
@@ -31,6 +31,11 @@ module DeployAngel
31
31
  Only act on status "exercisable" or "waiting_for_activity"; for "warm_up" or
32
32
  "no_baseline" nothing you run can clear it. Exercise routes marked mutating only with a test account
33
33
  or after asking. Report what you ran with the plan's report_with command (deployangel check).
34
+ A failing volume_drop or checkpoint_rate finding with an accept_url may be an intended change, such as
35
+ a route or job you removed on purpose. You cannot accept it, and must not try: tell the user why you
36
+ think it was intended, with your evidence, and give them the accept_url so a person decides. Until
37
+ someone accepts it, the release is failed: never report it as success. A finding with status
38
+ accepted was accepted as intended by the person in accepted_as_intended.
34
39
  TEXT
35
40
 
36
41
  TARGET_PROPERTIES = {
@@ -117,7 +122,7 @@ module DeployAngel
117
122
  TARGET_PROPERTIES),
118
123
  tool("list_deployments", "Recent deployments with their verification state and verdict.",
119
124
  { "limit" => { "type" => "integer", "minimum" => 1, "maximum" => 50, "default" => 10 } }),
120
- tool("get_exception", "Sanitized details and the application stack trace for an exception fingerprint.",
125
+ tool("get_exception", "Sanitized details and a representative stack trace (application frames, or library frames when there are none) for an exception fingerprint.",
121
126
  { "fingerprint" => { "type" => "string" } }, required: %w[fingerprint]),
122
127
  tool("list_late_regressions", "Failures found after a release was cleared, on paths the clearance did not cover.",
123
128
  { "since" => { "type" => "string", "description" => "ISO 8601 time" },
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.36"
4
+ VERSION = "0.1.38"
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.36
4
+ version: 0.1.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens