deployangel 0.1.43 → 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: ce8544c5aa3368fc5df3c437726bcb157edec54cfa43b90814c3ae0b4e5777dc
4
- data.tar.gz: c2cd5fec697b3fc987043ab6aaaf361d34050cd4979ee397dfcf452b3b94dcf4
3
+ metadata.gz: 6808537b2a27128d5b5c8770015db33cdf62b708776649485f2113ecd5a013d9
4
+ data.tar.gz: 001e46db33f806a94016b93f6af3dfa7b5709eb923f4918197d6ba2d444120ad
5
5
  SHA512:
6
- metadata.gz: 0ca495f1e92256a66a96e5b020baaeb75680904be1a89407ba9250e1396f64c3d9441b02b6b59968e3c453c827c4cf76f5850ae405a084ed3567c6ce776e9d82
7
- data.tar.gz: 0e6ff274dbf1cf28bea67fbc6ab46f13009ebeef1b3d9a7967ceaa874363ec195f99ee3e2144423107594f10a256fd6483c916d3ed65155a1e0fee8427928683
6
+ metadata.gz: d08170a8aeb9620b47db7630b95e016c415c11afb04a6c7bc7ea79cac1703e7c1a34c86dddcf73c8385cb258e31f39ca651f3e1339068df544248f507533c9e5
7
+ data.tar.gz: cdf1c1fe799b63e653abe0309332798d90607d6a3631527acb74669a8689897662870627062e95bd466ed3c70959e28bbe8c87cc8f81a4984f7c458d5726af6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## 0.1.43 (2026-11-05)
4
17
 
5
18
  - The MCP server's instructions, and the block `deployangel install agents`
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
@@ -136,6 +138,12 @@ module DeployAngel
136
138
  and `list_late_regressions` (or run `bundle exec deployangel status`). If a
137
139
  recent release failed, isn't cleared yet, or has a late regression, mention
138
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.
139
147
  #{AGENTS_END}
140
148
  MARKDOWN
141
149
 
@@ -185,6 +193,7 @@ module DeployAngel
185
193
  when "plan" then plan
186
194
  when "exercise" then exercise
187
195
  when "exception" then exception
196
+ when "context" then context
188
197
  when "check" then check
189
198
  when "install" then install
190
199
  when "doctor" then doctor
@@ -384,6 +393,28 @@ module DeployAngel
384
393
  0
385
394
  end
386
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
+
387
418
  def check
388
419
  options = parse(name: nil, status: nil, covers: [], details_url: nil) do |o, opts|
389
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
@@ -37,6 +37,11 @@ module DeployAngel
37
37
  evidence, and give them the accept_url so a person decides. Until someone accepts it, the release
38
38
  is failed: never report it as success. A finding with status
39
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.
40
45
  TEXT
41
46
 
42
47
  TARGET_PROPERTIES = {
@@ -128,7 +133,16 @@ module DeployAngel
128
133
  tool("list_late_regressions", "Failures found after a release was cleared, on paths the clearance did not cover. " \
129
134
  "One with dismissed set was dismissed by a person as a false alarm, with their reason: it needs no action.",
130
135
  { "since" => { "type" => "string", "description" => "ISO 8601 time" },
131
- "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." } })
132
146
  ]
133
147
  if scopes.include?("deployments")
134
148
  list << tool("register_deployment", "Register a deployment so it is verified (manual and CI deploys).",
@@ -161,6 +175,7 @@ module DeployAngel
161
175
  when "get_exception" then @client.exception(arguments.fetch("fingerprint") { raise ArgumentError, "fingerprint is required" })
162
176
  when "list_late_regressions"
163
177
  { "late_regressions" => @client.late_regressions(since: arguments["since"], limit: arguments.fetch("limit", 10)) }
178
+ when "get_production_context" then production_context(arguments)
164
179
  when "register_deployment"
165
180
  raise ArgumentError, "register_deployment is not available for this token" unless scopes.include?("deployments")
166
181
 
@@ -188,6 +203,15 @@ module DeployAngel
188
203
  "in_progress" => outcome.exit_code == VerificationWaiter::TIMED_OUT, "verification" => outcome.document }
189
204
  end
190
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
+
191
215
  def plan_content(outcome)
192
216
  return { "found" => false, "note" => "No matching deployment is registered yet." } if outcome.not_found
193
217
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.43"
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.43
4
+ version: 0.1.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens