deployangel 0.1.42 → 0.1.44
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/README.md +18 -2
- data/lib/deployangel/cli/formatter.rb +34 -0
- data/lib/deployangel/cli.rb +37 -5
- data/lib/deployangel/client.rb +7 -0
- data/lib/deployangel/mcp.rb +30 -5
- 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: 6808537b2a27128d5b5c8770015db33cdf62b708776649485f2113ecd5a013d9
|
|
4
|
+
data.tar.gz: 001e46db33f806a94016b93f6af3dfa7b5709eb923f4918197d6ba2d444120ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d08170a8aeb9620b47db7630b95e016c415c11afb04a6c7bc7ea79cac1703e7c1a34c86dddcf73c8385cb258e31f39ca651f3e1339068df544248f507533c9e5
|
|
7
|
+
data.tar.gz: cdf1c1fe799b63e653abe0309332798d90607d6a3631527acb74669a8689897662870627062e95bd466ed3c70959e28bbe8c87cc8f81a4984f7c458d5726af6a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.44 (2026-11-14)
|
|
4
|
+
|
|
5
|
+
- `deployangel context <files>` and the MCP server's `get_production_context`
|
|
6
|
+
say what production does with code before you change it: the routes and
|
|
7
|
+
jobs the files serve, how often production ran them in the last week and
|
|
8
|
+
how many failed, whether a release's clearance would cover a change to them
|
|
9
|
+
at this time of day, their schedules and critical flows, and failed
|
|
10
|
+
releases and late regressions on them in the last 90 days. Files it can't
|
|
11
|
+
map are listed as unmapped, never as unused. The MCP server's instructions,
|
|
12
|
+
and the block `deployangel install agents` writes, tell coding agents to
|
|
13
|
+
call it before changing code that serves requests or runs as a job
|
|
14
|
+
(servers from 2026-11-14).
|
|
15
|
+
|
|
16
|
+
## 0.1.43 (2026-11-05)
|
|
17
|
+
|
|
18
|
+
- The MCP server's instructions, and the block `deployangel install agents`
|
|
19
|
+
writes, say a failing `p95_latency` finding with an `accept_url` may be an
|
|
20
|
+
intended slowdown, such as a page that now does more work: as with a volume
|
|
21
|
+
or checkpoint drop, a coding agent hands the link to a person with its
|
|
22
|
+
evidence, never accepts it (servers from 2026-11-05).
|
|
23
|
+
|
|
3
24
|
## 0.1.42 (2026-11-05)
|
|
4
25
|
|
|
5
26
|
- `deployangel verify` marks a late regression that someone dismissed as a
|
data/README.md
CHANGED
|
@@ -517,6 +517,7 @@ bundle exec deployangel exercise --url=https://example.com # send the plan's re
|
|
|
517
517
|
bundle exec deployangel release --commit=$SHA # register a deploy (manual or CI)
|
|
518
518
|
bundle exec deployangel check --name="smoke: signup" --status=pass --covers=registration
|
|
519
519
|
bundle exec deployangel exception <fingerprint>
|
|
520
|
+
bundle exec deployangel context app/jobs/refund_job.rb # what production does with code, before changing it
|
|
520
521
|
bundle exec deployangel doctor # check the setup
|
|
521
522
|
```
|
|
522
523
|
|
|
@@ -580,8 +581,23 @@ server needs `DEPLOYANGEL_API_TOKEN` in the environment it starts in.
|
|
|
580
581
|
|
|
581
582
|
Tools: `get_verification`, `wait_for_verification` (up to 5 minutes per call),
|
|
582
583
|
`get_exercise_plan`, `list_deployments`, `get_exception`,
|
|
583
|
-
`list_late_regressions`, and `register_deployment`
|
|
584
|
-
The tools are read-only with respect to production.
|
|
584
|
+
`list_late_regressions`, `get_production_context`, and `register_deployment`
|
|
585
|
+
when the token allows it. The tools are read-only with respect to production.
|
|
586
|
+
|
|
587
|
+
### Before changing code
|
|
588
|
+
|
|
589
|
+
`deployangel context <files>` (or the `get_production_context` MCP tool)
|
|
590
|
+
says what production does with code before you or your agent change it: the
|
|
591
|
+
routes and jobs the files serve, how often production ran them in the last
|
|
592
|
+
week and how many failed, whether a release's clearance would cover a
|
|
593
|
+
change to them at this time of day, their schedules and critical flows, and
|
|
594
|
+
failed releases and late regressions on them in the last 90 days. A rarely
|
|
595
|
+
used route won't be covered by a clearance, so that's when to plan a smoke
|
|
596
|
+
test or `deployangel exercise`. Files map through the routes and jobs the
|
|
597
|
+
agent reported and through stack traces; models and libraries often map
|
|
598
|
+
only through the exceptions raised in them, and a file DeployAngel can't map
|
|
599
|
+
is listed as unmapped, never as unused. Pass `--route` or `--job` to ask
|
|
600
|
+
about a route or job class by name.
|
|
585
601
|
|
|
586
602
|
### Clearing quiet releases sooner
|
|
587
603
|
|
|
@@ -229,6 +229,40 @@ module DeployAngel
|
|
|
229
229
|
end
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
+
OUTLOOKS = { "normal_traffic" => "normal traffic covers it", "later_today" => "runs later today",
|
|
233
|
+
"rarely_used" => "rarely used: a clearance won't cover it", "unknown" => "outlook unknown" }.freeze
|
|
234
|
+
|
|
235
|
+
# Production context, one block per route or job.
|
|
236
|
+
def production_context(details)
|
|
237
|
+
lines = []
|
|
238
|
+
Array(details["items"]).each do |item|
|
|
239
|
+
uses = "#{item["uses_last_7_days"]} #{item["kind"] == "route" ? "requests" : "runs"} in 7 days"
|
|
240
|
+
failures = item["failures_last_7_days"].to_i.positive? ? ", #{item["failures_last_7_days"]} failed" : ""
|
|
241
|
+
lines << "#{item["kind"] == "route" ? "Route" : "Job"} #{item["key"]}: #{uses}#{failures} · " \
|
|
242
|
+
"#{OUTLOOKS.fetch(item.dig("clearance", "outlook"), item.dig("clearance", "outlook"))}"
|
|
243
|
+
lines << " #{item.dig("clearance", "note")}" if item.dig("clearance", "outlook") != "normal_traffic"
|
|
244
|
+
lines << " Pending first use since an earlier release" if item["pending_first_use"]
|
|
245
|
+
if (schedule = item["schedule"])
|
|
246
|
+
lines << " Scheduled #{schedule["schedule"]}#{", next #{schedule["next_at"]}" if schedule["next_at"]} (#{schedule["status"]})"
|
|
247
|
+
end
|
|
248
|
+
lines << " Critical flows: #{item["critical_flows"].join(", ")}" if Array(item["critical_flows"]).any?
|
|
249
|
+
Array(item["failed_releases"]).each { |failed| lines << " Failed #{failed["release"]}: #{failed["label"]}" }
|
|
250
|
+
Array(item["late_regressions"]).each do |late|
|
|
251
|
+
lines << " Late regression in #{late["release"]}#{" (dismissed)" if late["dismissed"]}: #{late["summary"]}"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
Array(details["files"]).each do |file|
|
|
255
|
+
Array(file["exceptions"]).each do |exception|
|
|
256
|
+
lines << "#{file["path"]}: #{exception["exception_class"]} in #{exception["top_frame"]}, " \
|
|
257
|
+
"#{exception["occurrences_last_7_days"]} in 7 days (fingerprint #{exception["fingerprint"]})"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
unmapped = Array(details["unmapped_files"])
|
|
261
|
+
lines << "Unmapped (not known to be unused): #{unmapped.join(", ")}" if unmapped.any?
|
|
262
|
+
lines << "Nothing found for these." if lines.empty?
|
|
263
|
+
(lines + Array(details["notes"]).map { |note| "Note: #{note}" }).join("\n")
|
|
264
|
+
end
|
|
265
|
+
|
|
232
266
|
def exception(details)
|
|
233
267
|
[ "#{details["exception_class"]}: #{details["message"]}",
|
|
234
268
|
"Fingerprint #{details["fingerprint"]} · first seen #{details["first_seen_at"]} (#{details["first_seen_release"]})",
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -20,7 +20,7 @@ module DeployAngel
|
|
|
20
20
|
# Runs without booting Rails.
|
|
21
21
|
class CLI
|
|
22
22
|
USAGE_ERROR = 5
|
|
23
|
-
COMMANDS = %w[release verify status plan exercise exception check install doctor setup token mcp version help].freeze
|
|
23
|
+
COMMANDS = %w[release verify status plan exercise exception context check install doctor setup token mcp version help].freeze
|
|
24
24
|
|
|
25
25
|
HELP = <<~TEXT
|
|
26
26
|
Usage: deployangel <command> [options]
|
|
@@ -37,6 +37,8 @@ module DeployAngel
|
|
|
37
37
|
requests to production [--param=NAME=VALUE] [--header="Name: value"]
|
|
38
38
|
[--max-requests=200] [--dry-run]
|
|
39
39
|
exception Details for a fingerprint deployangel exception FINGERPRINT
|
|
40
|
+
context What production does with deployangel context FILE... [--route="POST /x"] [--job=Job]
|
|
41
|
+
code, before you change it [--format=text|json]
|
|
40
42
|
check Report a smoke test result --name=NAME --status=pass|fail [--covers=a,b]
|
|
41
43
|
[--commit=SHA | --deployment=ID] [--details-url=URL]
|
|
42
44
|
install Add a Kamal deploy hook, deployangel install kamal
|
|
@@ -112,10 +114,11 @@ module DeployAngel
|
|
|
112
114
|
investigate the likely cause, and propose a fix. `next_steps` names the last
|
|
113
115
|
cleared release a rollback would go to. Do not roll back or change
|
|
114
116
|
production without explicit approval.
|
|
115
|
-
- A failed release whose failing finding is a volume or checkpoint drop
|
|
116
|
-
made on purpose (a route or job you removed
|
|
117
|
-
|
|
118
|
-
finding's `accept_url` so a
|
|
117
|
+
- A failed release whose failing finding is a volume or checkpoint drop or a
|
|
118
|
+
slowdown you made on purpose (a route or job you removed, a page that now
|
|
119
|
+
does more work): you can't accept it. Tell the user why you think it was
|
|
120
|
+
intended, with your evidence, and give them the finding's `accept_url` so a
|
|
121
|
+
person decides. It stays failed until then.
|
|
119
122
|
- Exit 3: still verifying; run the command again.
|
|
120
123
|
- Not cleared yet: call `get_exercise_plan` (or `bundle exec deployangel plan`).
|
|
121
124
|
If its status is "exercisable" or "waiting_for_activity", run
|
|
@@ -135,6 +138,12 @@ module DeployAngel
|
|
|
135
138
|
and `list_late_regressions` (or run `bundle exec deployangel status`). If a
|
|
136
139
|
recent release failed, isn't cleared yet, or has a late regression, mention
|
|
137
140
|
it before other work.
|
|
141
|
+
- Before changing code that serves requests or runs as a job: call
|
|
142
|
+
`get_production_context` with the files you plan to change (or run
|
|
143
|
+
`bundle exec deployangel context <files>`). If an item's clearance outlook
|
|
144
|
+
is `rarely_used` or `later_today`, a clearance won't cover your change soon
|
|
145
|
+
after deploying: say so, and plan to exercise it or report a check that
|
|
146
|
+
covers it. Files in `unmapped_files` aren't known to be unused.
|
|
138
147
|
#{AGENTS_END}
|
|
139
148
|
MARKDOWN
|
|
140
149
|
|
|
@@ -184,6 +193,7 @@ module DeployAngel
|
|
|
184
193
|
when "plan" then plan
|
|
185
194
|
when "exercise" then exercise
|
|
186
195
|
when "exception" then exception
|
|
196
|
+
when "context" then context
|
|
187
197
|
when "check" then check
|
|
188
198
|
when "install" then install
|
|
189
199
|
when "doctor" then doctor
|
|
@@ -383,6 +393,28 @@ module DeployAngel
|
|
|
383
393
|
0
|
|
384
394
|
end
|
|
385
395
|
|
|
396
|
+
# What production does with some code, before changing it (spec §27,
|
|
397
|
+
# Production context).
|
|
398
|
+
def context
|
|
399
|
+
options = parse(routes: [], job_classes: [], format: nil) do |o, opts|
|
|
400
|
+
o.on("--route=ROUTE") { |v| opts[:routes] << v }
|
|
401
|
+
o.on("--job=JOB_CLASS") { |v| opts[:job_classes] << v }
|
|
402
|
+
o.on("--format=FORMAT", %w[text json]) { |v| opts[:format] = v }
|
|
403
|
+
end
|
|
404
|
+
files = @argv.dup
|
|
405
|
+
@argv.clear
|
|
406
|
+
if files.empty? && options[:routes].empty? && options[:job_classes].empty?
|
|
407
|
+
return usage_error("context needs files, --route, or --job, like `deployangel context app/jobs/refund_job.rb`")
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
details = client.production_context(files: files, routes: options[:routes], job_classes: options[:job_classes])
|
|
411
|
+
output(details, options[:format]) { Formatter.production_context(details) }
|
|
412
|
+
0
|
|
413
|
+
rescue Client::NotFound
|
|
414
|
+
@stderr.puts("deployangel: this DeployAngel server doesn't offer production context yet")
|
|
415
|
+
USAGE_ERROR
|
|
416
|
+
end
|
|
417
|
+
|
|
386
418
|
def check
|
|
387
419
|
options = parse(name: nil, status: nil, covers: [], details_url: nil) do |o, opts|
|
|
388
420
|
o.on("--name=NAME") { |v| opts[:name] = v }
|
data/lib/deployangel/client.rb
CHANGED
|
@@ -40,6 +40,13 @@ module DeployAngel
|
|
|
40
40
|
def exception(fingerprint) = get("/api/v1/exceptions/#{URI.encode_www_form_component(fingerprint)}")
|
|
41
41
|
def late_regressions(**filters) = get("/api/v1/late_regressions", filters.compact)["late_regressions"]
|
|
42
42
|
|
|
43
|
+
# What production does with some code, before a coding agent changes it
|
|
44
|
+
# (spec §27, Production context).
|
|
45
|
+
def production_context(files: [], routes: [], job_classes: [])
|
|
46
|
+
get("/api/v1/production_context",
|
|
47
|
+
{ "files[]" => files, "routes[]" => routes, "job_classes[]" => job_classes }.reject { |_, values| values.empty? })
|
|
48
|
+
end
|
|
49
|
+
|
|
43
50
|
def verification(deployment_id, all_findings: false)
|
|
44
51
|
get("/api/v1/deployments/#{deployment_id}/verification", all_findings ? { findings: "all" } : {})
|
|
45
52
|
end
|
data/lib/deployangel/mcp.rb
CHANGED
|
@@ -31,11 +31,17 @@ 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
|
|
35
|
-
a route or job you removed on purpose
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
A failing volume_drop, checkpoint_rate, or p95_latency finding with an accept_url may be an intended
|
|
35
|
+
change, such as a route or job you removed on purpose, or a page that now does more work.
|
|
36
|
+
You cannot accept it, and must not try: tell the user why you think it was intended, with your
|
|
37
|
+
evidence, and give them the accept_url so a person decides. Until someone accepts it, the release
|
|
38
|
+
is failed: never report it as success. A finding with status
|
|
38
39
|
accepted was accepted as intended by the person in accepted_as_intended.
|
|
40
|
+
Before changing code that serves requests or runs as a job, call get_production_context with the files
|
|
41
|
+
you plan to change. It says which routes and jobs they serve and how often production runs them. If an
|
|
42
|
+
item's clearance outlook is rarely_used or later_today, a clearance won't cover your change soon after
|
|
43
|
+
deploying: tell the user, and plan to exercise it or report a check that covers it. unmapped_files are
|
|
44
|
+
files DeployAngel can't map to routes or jobs: never take that to mean they're unused.
|
|
39
45
|
TEXT
|
|
40
46
|
|
|
41
47
|
TARGET_PROPERTIES = {
|
|
@@ -127,7 +133,16 @@ module DeployAngel
|
|
|
127
133
|
tool("list_late_regressions", "Failures found after a release was cleared, on paths the clearance did not cover. " \
|
|
128
134
|
"One with dismissed set was dismissed by a person as a false alarm, with their reason: it needs no action.",
|
|
129
135
|
{ "since" => { "type" => "string", "description" => "ISO 8601 time" },
|
|
130
|
-
"limit" => { "type" => "integer", "minimum" => 1, "maximum" => 50, "default" => 10 } })
|
|
136
|
+
"limit" => { "type" => "integer", "minimum" => 1, "maximum" => 50, "default" => 10 } }),
|
|
137
|
+
tool("get_production_context", "What production does with code before you change it: the routes and jobs the given " \
|
|
138
|
+
"files serve, how often production ran them in the last 7 days and how many failed, whether a release's clearance " \
|
|
139
|
+
"would cover a change to them (clearance.outlook: normal_traffic, later_today, rarely_used, or unknown), their " \
|
|
140
|
+
"schedules and critical flows, and failed releases and late regressions on them in the last 90 days. " \
|
|
141
|
+
"Files it can't map are listed in unmapped_files; that doesn't mean they're unused.",
|
|
142
|
+
{ "files" => { "type" => "array", "items" => { "type" => "string" },
|
|
143
|
+
"description" => "Repository-relative paths, e.g. app/jobs/refund_job.rb (up to 50)." },
|
|
144
|
+
"routes" => { "type" => "array", "items" => { "type" => "string" }, "description" => "Route keys, e.g. \"POST /refunds\"." },
|
|
145
|
+
"job_classes" => { "type" => "array", "items" => { "type" => "string" }, "description" => "Job class names, e.g. RefundJob." } })
|
|
131
146
|
]
|
|
132
147
|
if scopes.include?("deployments")
|
|
133
148
|
list << tool("register_deployment", "Register a deployment so it is verified (manual and CI deploys).",
|
|
@@ -160,6 +175,7 @@ module DeployAngel
|
|
|
160
175
|
when "get_exception" then @client.exception(arguments.fetch("fingerprint") { raise ArgumentError, "fingerprint is required" })
|
|
161
176
|
when "list_late_regressions"
|
|
162
177
|
{ "late_regressions" => @client.late_regressions(since: arguments["since"], limit: arguments.fetch("limit", 10)) }
|
|
178
|
+
when "get_production_context" then production_context(arguments)
|
|
163
179
|
when "register_deployment"
|
|
164
180
|
raise ArgumentError, "register_deployment is not available for this token" unless scopes.include?("deployments")
|
|
165
181
|
|
|
@@ -187,6 +203,15 @@ module DeployAngel
|
|
|
187
203
|
"in_progress" => outcome.exit_code == VerificationWaiter::TIMED_OUT, "verification" => outcome.document }
|
|
188
204
|
end
|
|
189
205
|
|
|
206
|
+
def production_context(arguments)
|
|
207
|
+
files, routes, job_classes = %w[files routes job_classes].map { |key| Array(arguments[key]).map(&:to_s).reject(&:empty?) }
|
|
208
|
+
raise ArgumentError, "give files, routes, or job_classes" if [ files, routes, job_classes ].all?(&:empty?)
|
|
209
|
+
|
|
210
|
+
@client.production_context(files: files, routes: routes, job_classes: job_classes)
|
|
211
|
+
rescue Client::NotFound
|
|
212
|
+
{ "note" => "This DeployAngel server doesn't offer production context yet." }
|
|
213
|
+
end
|
|
214
|
+
|
|
190
215
|
def plan_content(outcome)
|
|
191
216
|
return { "found" => false, "note" => "No matching deployment is registered yet." } if outcome.not_found
|
|
192
217
|
|
data/lib/deployangel/version.rb
CHANGED