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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33fd1151c7e090754dcfa51ced575d52281964abe14f285b46744c2d7c7f8b5c
4
- data.tar.gz: 1e54552a26d6935c70c8abc92860128af5eaacb7545fc6f555d47508e2f81d7f
3
+ metadata.gz: 6808537b2a27128d5b5c8770015db33cdf62b708776649485f2113ecd5a013d9
4
+ data.tar.gz: 001e46db33f806a94016b93f6af3dfa7b5709eb923f4918197d6ba2d444120ad
5
5
  SHA512:
6
- metadata.gz: 0465d87b3ebd9e3a67c7260fe71179c9d44baa01ec7772a77cfa8802f32a99991cf0267d98a65701f1b98fb0e6067b691bb7c5644bcdf148680d1d4d7ae69ddc
7
- data.tar.gz: 3c3231b3af88dc4831479309d8631a29b721d8c9527ce0cb6247974db7d6c4e35ded6e3b9cd0ca38228ce08b8c528f04f91349e2fb43b7cc581daab7403c8d63
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` when the token allows it.
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"]})",
@@ -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 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.
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 }
@@ -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
@@ -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 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
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.42"
4
+ VERSION = "0.1.44"
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.42
4
+ version: 0.1.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens