deployangel 0.1.6 → 0.1.8

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: 95e80de544815ec904e01198f948c81c722ddadb005f6e87761827ac1cb3a362
4
- data.tar.gz: 6175861126a88ed4bc09bdaf600c03e1ce5cd88fa3361c53d7c761ff7a066b38
3
+ metadata.gz: 26338624984d9d36012cf0e66b2448ddf31985ae9b629c4ca211e467c5e39e6c
4
+ data.tar.gz: bbbb651d96b7091563b263e20df94bc8471bf132bbf8a0511297176c111a74ed
5
5
  SHA512:
6
- metadata.gz: 9d20de4305540d12d4b24223c7f4eda020b0855b8205cbeac03c51bf8befc701cb783a1e4600c3781b7d15b906a739aa63988bacc4864bc15a286e32c0138405
7
- data.tar.gz: 38682cdc5941ad48af3ed324356d5bf71309bc5fe2c41fd0c582198ce0315c1863575f5896b13516b634cf6d56e69aee1124a3491c2fa4aae28c49f4b6a77a59
6
+ metadata.gz: 5846a9913395c0defeeb41b1e5f5ad20a924439ee92ca899b09ae76dcbf935748432cf2c02e01b81772029796f39f5cc221c690313b30ea73a2831aac11825d0
7
+ data.tar.gz: 534d6218818cb03b8be1f4144d21d129484b112acdb08a18c53300f62c67dd7198d9dc48a66284ada08069ea08a0795f9cce705d8c6baa11f4fc371d5edf88e5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8 (2026-10-04)
4
+
5
+ - Health checks served by a lambda or a mounted Rack app at a conventional
6
+ path (`/up`, `/health`, `/healthz`, `/healthcheck`, `/health_check`,
7
+ `/livez`, `/readyz`, `/statusz`, `/ping`) are left out, like the health-check
8
+ gems'. A controller at one of these paths is still recorded, since it may be
9
+ a real page.
10
+ - `config.sidekiq_cron_schedule_file` reads sidekiq-cron jobs from the file an
11
+ app loads them from itself, such as `config/sidekiq_schedule.yml.erb` passed
12
+ to `Sidekiq::Cron::Job.load_from_hash!`. ERB is rendered, as in the default
13
+ file.
14
+
15
+ ## 0.1.7 (2026-10-04)
16
+
17
+ - Health checks from OkComputer, health_check, and rails-healthcheck are left
18
+ out like Rails' own, wherever they're mounted.
19
+ - `config.ignored_routes` leaves out other routes, such as a health check
20
+ served by your own controller: `[ "GET /healthz" ]`. HEAD requests to a
21
+ listed GET route are left out too, and listed routes aren't sent in the
22
+ route table, so DeployAngel never expects them to run.
23
+
3
24
  ## 0.1.6 (2026-10-04)
4
25
 
5
26
  - Requests to Rails' health check (`rails/health#show`, at `/up` by default)
data/README.md CHANGED
@@ -45,8 +45,10 @@ ENV DEPLOYANGEL_REVISION=$GIT_SHA
45
45
 
46
46
  and build with `docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .`. On
47
47
  DigitalOcean App Platform, set `DEPLOYANGEL_REVISION: ${_self.COMMIT_HASH}` in
48
- the app spec. If the agent finds none of these, its telemetry can't be tied to a
49
- deploy, and the dashboard says so.
48
+ the app spec. On Elastic Beanstalk, see
49
+ [Deploying to Elastic Beanstalk](#deploying-to-elastic-beanstalk). If the agent
50
+ finds none of these, its telemetry can't be tied to a deploy, and the dashboard
51
+ says so.
50
52
 
51
53
  ## What it sends
52
54
 
@@ -73,6 +75,17 @@ deploy, and the dashboard says so.
73
75
  It does not send request bodies, parameters, headers, cookies, SQL, logs, or
74
76
  user data.
75
77
 
78
+ ## Data and pricing
79
+
80
+ - Everything goes to DeployAngel's hosted service, which runs on DigitalOcean
81
+ in the United States. There's no self-hosted version, and no choice of
82
+ region.
83
+ - Telemetry is kept for 21 days, and release history for 7, 30, or 90 days
84
+ depending on the plan. The [privacy policy](https://deployangel.com/privacy)
85
+ has the details and the services DeployAngel uses.
86
+ - DeployAngel is free during the beta, with notice before paid plans start.
87
+ See [pricing](https://deployangel.com/pricing).
88
+
76
89
  ## Safety
77
90
 
78
91
  - Nothing runs on the network during a request. Requests only update
@@ -111,6 +124,38 @@ DeployAngel.configure do |config|
111
124
  end
112
125
  ```
113
126
 
127
+ Health checks aren't recorded: load balancers and uptime monitors call them
128
+ all the time and they always answer fast, so they would make your app look
129
+ busier and healthier than its real pages. The agent recognizes Rails' own
130
+ (`/up`), OkComputer, health_check, and rails-healthcheck, wherever they're
131
+ mounted, and a lambda or Rack app at a conventional path (`/up`, `/health`,
132
+ `/healthz`, `/healthcheck`, `/health_check`, `/livez`, `/readyz`, `/statusz`,
133
+ `/ping`), such as `get "healthz", to: ->(_) { [200, {}, ["ok"]] }`. If your
134
+ health check is your own controller, list its route the way the dashboard
135
+ shows it. HEAD requests to it are left out too:
136
+
137
+ ```ruby
138
+ DeployAngel.configure do |config|
139
+ config.ignored_routes = [ "GET /healthz" ]
140
+ end
141
+ ```
142
+
143
+ DeployAngel expects declared recurring jobs on schedule. It reads Solid Queue's
144
+ `config/recurring.yml`, sidekiq-scheduler's section of Sidekiq's config, and
145
+ sidekiq-cron's schedule file (`config/schedule.yml` unless sidekiq-cron is
146
+ configured otherwise), rendering ERB in each. If your app loads sidekiq-cron
147
+ jobs itself from another file, for example with `Sidekiq::Cron::Job.load_from_hash!`,
148
+ point the agent at that file:
149
+
150
+ ```ruby
151
+ DeployAngel.configure do |config|
152
+ config.sidekiq_cron_schedule_file = "config/sidekiq_schedule.yml.erb"
153
+ end
154
+ ```
155
+
156
+ Jobs that exist only in Redis, such as ones created in code or in the
157
+ Sidekiq web UI, aren't read.
158
+
114
159
  `DEPLOYANGEL_ENABLED=true|false` forces reporting on or off in any environment.
115
160
  `DEPLOYANGEL_URL` overrides the API endpoint (default `https://api.deployangel.com`).
116
161
 
@@ -169,6 +214,28 @@ to the run. Explicit options still win.
169
214
  DEPLOYANGEL_API_TOKEN: ${{ secrets.DEPLOYANGEL_API_TOKEN }}
170
215
  ```
171
216
 
217
+ ### When a release never reports
218
+
219
+ A registered release must start reporting within 15 minutes of being
220
+ registered. If no instance reports it by then, the verdict is
221
+ **inconclusive** with reason `no_release_telemetry`, and the dashboard says
222
+ "Not cleared: this release never reported". The usual causes are a release
223
+ that never booted (a failed migration or a crash on start), an agent with no
224
+ token, or a release identity the agent can't find. If the app kept reporting
225
+ an earlier release instead, the verdict names it ("while v41 kept running").
226
+ `deployangel verify --wait` exits 2, so the CI step above fails the job.
227
+
228
+ If the release does start reporting within 24 hours, as after a slow rolling
229
+ deploy, DeployAngel withdraws that verdict and verifies the release from then,
230
+ unless a newer release has rolled out first. A CI job that already failed stays
231
+ failed; run `deployangel verify --wait` again to wait for the new verdict.
232
+
233
+ Only registered releases are caught this way. DeployAngel learns about an
234
+ unregistered release when the agent first reports it, so a release that never
235
+ boots is never seen. If a release that dies on boot is a risk for you, register
236
+ deploys from CI. The 15 minutes count from registration, so register when the
237
+ deploy finishes, not when it starts.
238
+
172
239
  ### Deploying with Kamal
173
240
 
174
241
  The agent reads `KAMAL_VERSION`, so it knows its release with no setup. Pass
@@ -221,6 +288,35 @@ set :deployangel_register, false # turn it off, e.g. for a stage without De
221
288
  With `:deployangel_wait`, `cap` exits with an error if the release fails
222
289
  verification, which CI can act on. It never rolls anything back.
223
290
 
291
+ ### Deploying to Elastic Beanstalk
292
+
293
+ Elastic Beanstalk doesn't tell the app which commit it's running. Set
294
+ `DEPLOYANGEL_REVISION` in the same `update-environment` call that deploys the
295
+ new version, so the two always change together. This works on every platform,
296
+ Docker included, with no build args. Then wait for the environment and register
297
+ the release:
298
+
299
+ ```bash
300
+ aws elasticbeanstalk update-environment \
301
+ --environment-name "$EB_ENV" \
302
+ --version-label "$VERSION_LABEL" \
303
+ --option-settings "Namespace=aws:elasticbeanstalk:application:environment,OptionName=DEPLOYANGEL_REVISION,Value=$GITHUB_SHA"
304
+ aws elasticbeanstalk wait environment-updated --environment-names "$EB_ENV"
305
+
306
+ bundle exec deployangel release --commit="$GITHUB_SHA" --version="$VERSION_LABEL"
307
+ bundle exec deployangel verify --wait --until=initial
308
+ ```
309
+
310
+ `update-environment` returns as soon as the deploy starts, so don't skip the
311
+ wait: registering early starts the
312
+ [15-minute clock](#when-a-release-never-reports) before the new version is
313
+ running. If the deploy fails and Beanstalk rolls back, the environment still
314
+ returns to Ready, but the new commit never reports, so `verify` exits 2.
315
+
316
+ Leave `DEPLOYANGEL_RELEASE_VERSION` unset here. When the agent reports a
317
+ version, the release is matched by it, so it would have to equal `--version`
318
+ exactly. Matching by commit avoids that.
319
+
224
320
  ## CLI and coding agents
225
321
 
226
322
  The gem ships a `deployangel` command. It doesn't boot Rails. Give it a token
@@ -11,7 +11,7 @@ module DeployAngel
11
11
 
12
12
  attr_accessor :token, :endpoint, :enabled, :environments, :release_version, :revision,
13
13
  :flush_interval, :open_timeout, :read_timeout, :max_queued_payloads, :max_routes, :logger,
14
- :file_digests, :critical_flows
14
+ :file_digests, :critical_flows, :ignored_routes, :sidekiq_cron_schedule_file
15
15
 
16
16
  def initialize(env = ENV)
17
17
  @token = env["DEPLOYANGEL_TOKEN"]
@@ -28,6 +28,11 @@ module DeployAngel
28
28
  @logger = Logger.new($stderr, level: Logger::WARN, progname: "deployangel")
29
29
  @file_digests = parse_boolean(env["DEPLOYANGEL_FILE_DIGESTS"]) != false
30
30
  @critical_flows = {}
31
+ # Route keys as the dashboard shows them, such as "GET /healthz".
32
+ @ignored_routes = []
33
+ # For an app that loads sidekiq-cron's jobs itself (load_from_hash!)
34
+ # from its own file, such as "config/sidekiq_schedule.yml.erb".
35
+ @sidekiq_cron_schedule_file = nil
31
36
  end
32
37
 
33
38
  # Reports only with a token. By default only in the listed
@@ -39,6 +44,14 @@ module DeployAngel
39
44
  environments.include?(environment.to_s)
40
45
  end
41
46
 
47
+ # Whether requests to a route are left out. Rails answers HEAD with the
48
+ # GET route, so ignoring "GET /healthz" ignores "HEAD /healthz" too.
49
+ def ignored_route?(key)
50
+ return false if ignored_routes.empty?
51
+
52
+ ignored_routes.include?(key) || ignored_routes.include?(key.sub(/\AHEAD /, "GET "))
53
+ end
54
+
42
55
  private
43
56
  def parse_boolean(value)
44
57
  return nil if value.nil? || value.strip.empty?
@@ -7,10 +7,15 @@ module DeployAngel
7
7
  # never the raw path, which keeps IDs out of route keys.
8
8
  class Http
9
9
  FORMAT_SUFFIX = "(.:format)"
10
- # Rails' health check (/up by default). Load balancers and uptime
11
- # monitors call it all the time and it always answers fast, so
12
- # counting it would make any app look busy and healthy.
13
- HEALTH_CHECK_CONTROLLER = "rails/health"
10
+ # Health checks: Rails' own (/up by default), OkComputer, health_check,
11
+ # and rails-healthcheck, wherever they're mounted. Load balancers and
12
+ # uptime monitors call them all the time and they answer fast, so
13
+ # counting them would make any app look busy and healthy. Others can
14
+ # be left out with config.ignored_routes.
15
+ HEALTH_CHECK_CONTROLLERS = %w[rails/health ok_computer/ok_computer health_check/health_check healthcheck/healthchecks].freeze
16
+ # Health checks served by a lambda or a mounted Rack app rather than a
17
+ # controller, at a conventional path such as /healthz.
18
+ HEALTH_CHECK_PATHS = %w[up health healthz healthcheck health_check livez readyz statusz ping].freeze
14
19
 
15
20
  def initialize(app)
16
21
  @app = app
@@ -35,9 +40,10 @@ module DeployAngel
35
40
 
36
41
  private
37
42
  def record(env, status, started, unhandled:, exception: nil)
38
- return if env["action_dispatch.request.path_parameters"]&.dig(:controller) == HEALTH_CHECK_CONTROLLER
43
+ return if health_check?(env)
39
44
 
40
45
  route = route_key(env)
46
+ return if route && DeployAngel.configuration.ignored_route?(route)
41
47
  # Unrouted successes are static files and similar; unrouted
42
48
  # errors (such as routing 404s) still count.
43
49
  return if route.nil? && status.to_i < 400
@@ -54,6 +60,16 @@ module DeployAngel
54
60
  nil
55
61
  end
56
62
 
63
+ # A controller at a health-check path may be a real page, so only
64
+ # controllerless routes are judged by their path.
65
+ def health_check?(env)
66
+ controller = env["action_dispatch.request.path_parameters"]&.dig(:controller)
67
+ return HEALTH_CHECK_CONTROLLERS.include?(controller) if controller
68
+
69
+ pattern = route_pattern(env) or return false
70
+ HEALTH_CHECK_PATHS.include?(pattern.delete_suffix(FORMAT_SUFFIX).split("/").last)
71
+ end
72
+
57
73
  def route_key(env)
58
74
  method = env["REQUEST_METHOD"]
59
75
  if (pattern = route_pattern(env))
@@ -40,12 +40,13 @@ module DeployAngel
40
40
  @app.routes.routes.flat_map do |route|
41
41
  controller = route.defaults[:controller]
42
42
  next [] if controller.nil? || controller.start_with?("rails/", "active_storage/", "action_mailbox/")
43
+ next [] if Http::HEALTH_CHECK_CONTROLLERS.include?(controller)
43
44
 
44
45
  path = route.path.spec.to_s.delete_suffix(FORMAT_SUFFIX)
45
46
  route.verb.to_s.split("|").reject(&:empty?).map do |verb|
46
47
  { "key" => "#{verb} #{path}", "controller" => controller, "action" => route.defaults[:action].to_s }
47
48
  end
48
- end.uniq { |route| route["key"] }
49
+ end.uniq { |route| route["key"] }.reject { |route| @config.ignored_route?(route["key"]) }
49
50
  rescue StandardError
50
51
  []
51
52
  end
@@ -85,8 +86,10 @@ module DeployAngel
85
86
  end
86
87
 
87
88
  # sidekiq-cron jobs from its schedule file (config/schedule.yml unless
88
- # configured otherwise), which it loads when Sidekiq starts. Jobs
89
- # created in code live only in Redis, which the agent doesn't read.
89
+ # configured otherwise), which it loads when Sidekiq starts, or from
90
+ # the file in config.sidekiq_cron_schedule_file for an app that loads
91
+ # them itself. Jobs created in code live only in Redis, which the agent
92
+ # doesn't read.
90
93
  def sidekiq_cron_schedules
91
94
  return [] unless defined?(::Sidekiq::Cron)
92
95
 
@@ -104,7 +107,8 @@ module DeployAngel
104
107
  end
105
108
 
106
109
  def sidekiq_cron_schedule_file
107
- configured = ::Sidekiq::Cron.configuration.cron_schedule_file if ::Sidekiq::Cron.respond_to?(:configuration)
110
+ configured = @config.sidekiq_cron_schedule_file
111
+ configured ||= ::Sidekiq::Cron.configuration.cron_schedule_file if ::Sidekiq::Cron.respond_to?(:configuration)
108
112
  File.expand_path(configured || "config/schedule.yml", @root)
109
113
  rescue StandardError
110
114
  File.join(@root, "config", "schedule.yml")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.8"
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.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens