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 +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/deployangel/cli/formatter.rb +28 -5
- data/lib/deployangel/cli.rb +14 -0
- data/lib/deployangel/doctor.rb +28 -1
- data/lib/deployangel/mcp.rb +5 -0
- 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: d930ff0701b6c48f6793339eaff985418ae4a6b0332689e7c87d85047f80a082
|
|
4
|
+
data.tar.gz: a1848b93327d98249e6bd98acf595047b62b926604c9940ed92f17334db3b093
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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|
|
|
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
|
|
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.
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -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)
|
data/lib/deployangel/doctor.rb
CHANGED
|
@@ -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
|
data/lib/deployangel/mcp.rb
CHANGED
|
@@ -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 = {
|
data/lib/deployangel/version.rb
CHANGED