deployangel 0.1.37 → 0.1.39

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: 618c24521dbd9aa6102fdd7c3a6c105494abcde74072fa37d8bd2e27f2737f49
4
- data.tar.gz: 55e402796d95fe4aee557b342fcb0814933b9f39c965bba504b160fdaf557c0d
3
+ metadata.gz: d930ff0701b6c48f6793339eaff985418ae4a6b0332689e7c87d85047f80a082
4
+ data.tar.gz: a1848b93327d98249e6bd98acf595047b62b926604c9940ed92f17334db3b093
5
5
  SHA512:
6
- metadata.gz: 4df4ece416d4c1d16729bbaaff61c0e196d3635796f1a743b754b189d91109891c38232bd484e1240fa55a28db6c85898a3b550fd3bd781337de4f05d18ecfc3
7
- data.tar.gz: 4a523fc4d15f677b80671a3a09490e267ad1c322fcd0b45d7f2c37dd68ea54586c36efd6204e22f2d20cea15185a2d2072c53bf5f0857fa5011442198ad61431
6
+ metadata.gz: '00943ef2bac8f6acb697ec20243c211b96779b4eb33dc8c4da1aff53cd789b14ca569320fa06d960de7811a0dd15e4aacab06e6656f4ffe1240a8b63cf1467eb'
7
+ data.tar.gz: 9e8ede3e14497f6afaf02b08f0438876397ee73b95f7ba79bbff8d43dc84ad69e018e0bffb903ce722458b1a5df48c2eb372580c034699a00315f09335a92235
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.39 (2026-11-04)
4
+
5
+ - `deployangel doctor` warns when the project's AGENTS.md or CLAUDE.md has
6
+ an older copy of the block `deployangel install agents` writes, and
7
+ `deployangel status` says so on stderr at the start of a session, where
8
+ a coding agent reads it. Running `deployangel install agents` updates
9
+ it. Hand-written instructions without the block's markers aren't
10
+ checked.
11
+
12
+ ## 0.1.38 (2026-11-04)
13
+
14
+ - `deployangel verify` shows a volume drop (servers from 2026-11-04) as
15
+ what ran against what was expected (`0 requests, about 27 expected`),
16
+ and lists findings a person accepted as an intended change, with who
17
+ and why. A failing finding that could be intended gets the link where a
18
+ person can accept it; the CLI and MCP server can't, so an agent never
19
+ accepts its own failure.
20
+ - The MCP server's instructions, and the block `deployangel install
21
+ agents` writes, tell agents to hand that link to the user with their
22
+ evidence and never report the release as a success.
23
+
3
24
  ## 0.1.37 (2026-11-03)
4
25
 
5
26
  - An exception raised entirely in gems, with no frame from the app's
@@ -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
@@ -254,6 +258,7 @@ module DeployAngel
254
258
  output(document, options[:format]) { Formatter.verification(document) }
255
259
  step_summary(Formatter.markdown(document))
256
260
  @stderr.puts("deployangel: timed out; verification is still in progress") if outcome.timed_out
261
+ stale_instructions_note if status
257
262
  outcome.exit_code
258
263
  end
259
264
 
@@ -402,6 +407,15 @@ module DeployAngel
402
407
  # What this checkout will report and what DeployAngel has seen of the
403
408
  # app. Warnings are advice, so only errors (a rejected token, an
404
409
  # unreachable server, no agent in the bundle) fail it.
410
+ # At the start of a session (`status`), where an agent reads it and can
411
+ # update the instructions it's following.
412
+ def stale_instructions_note
413
+ stale = Doctor.stale_instructions(@root)
414
+ return if stale.empty?
415
+
416
+ @stderr.puts("deployangel: #{Doctor.stale_instructions_note(stale)}; run `deployangel install agents` to update")
417
+ end
418
+
405
419
  def doctor
406
420
  format = parse(format: nil) { |o, opts| o.on("--format=FORMAT", %w[text json]) { |v| opts[:format] = v } }[:format]
407
421
  doctor = Doctor.new(root: @root, client: doctor_client, env: @env)
@@ -36,10 +36,29 @@ module DeployAngel
36
36
  def checks
37
37
  @checks ||= begin
38
38
  @setup, server = server_checks
39
- [ *agent_checks, here_check, schedule_check, *server, release_check, *comparison_checks ].compact
39
+ [ *agent_checks, here_check, schedule_check, *server, release_check, *comparison_checks, instructions_check ].compact
40
40
  end
41
41
  end
42
42
 
43
+ # Files whose DeployAngel block (between the markers `deployangel install
44
+ # agents` writes) differs from this version's: agents keep following the
45
+ # older copy until it's updated. Hand-written instructions without the
46
+ # markers aren't checked.
47
+ def self.stale_instructions(root)
48
+ block = /#{Regexp.escape(CLI::AGENTS_START)}.*?#{Regexp.escape(CLI::AGENTS_END)}/m
49
+ %w[AGENTS.md CLAUDE.md].select do |name|
50
+ path = File.join(root, name)
51
+ found = File.file?(path) && File.read(path)[block]
52
+ found && found != CLI::AGENT_INSTRUCTIONS.chomp
53
+ end
54
+ rescue SystemCallError
55
+ []
56
+ end
57
+
58
+ def self.stale_instructions_note(files)
59
+ "#{files.join(" and ")} #{files.one? ? "has" : "have"} an older copy of DeployAngel's agent instructions"
60
+ end
61
+
43
62
  def failed?
44
63
  checks.any? { |check| check.status == :error }
45
64
  end
@@ -57,6 +76,14 @@ module DeployAngel
57
76
  [ Check.new(status: :error, message: "The agent isn't in the bundle", fix: "Add gem \"deployangel\" to the Gemfile and run bundle install.") ]
58
77
  end
59
78
 
79
+ def instructions_check
80
+ stale = self.class.stale_instructions(@root)
81
+ return if stale.empty?
82
+
83
+ Check.new(status: :warn, message: self.class.stale_instructions_note(stale),
84
+ fix: "Run deployangel install agents to update #{stale.one? ? "it" : "them"}.")
85
+ end
86
+
60
87
  def schedule_check
61
88
  counts = local_schedules.group_by { |schedule| schedule["source"] }.map { |source, list| "#{SOURCES.fetch(source, source)} #{list.size}" }
62
89
  later = [ ("GoodJob's cron" if read("Gemfile.lock").to_s.include?(" good_job (")), "config.recurring_jobs" ].compact
@@ -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 = {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.37"
4
+ VERSION = "0.1.39"
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.37
4
+ version: 0.1.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens