deployangel 0.1.21 → 0.1.22
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 +14 -0
- data/README.md +37 -0
- data/lib/deployangel/configuration.rb +8 -1
- data/lib/deployangel/metadata.rb +67 -1
- data/lib/deployangel/rails/railtie.rb +11 -1
- data/lib/deployangel/tasks.rb +74 -0
- data/lib/deployangel/version.rb +1 -1
- data/lib/deployangel.rb +10 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edccb6c72eff7d259c14c59deaa403fb78c890cb091b40d59b080623945e1e9f
|
|
4
|
+
data.tar.gz: 364fd681c6199d591292fc67799cb6f7933da99736113ef382ff4ccf628697f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ec79d7a8b20600e3fc57604b990749ff578a6b39bdd2caf945c8f60bf5f39ed74f3991659f8d3e26c851e8a7509cc4dabca339f36f35aaa79aca33648ba4c9d
|
|
7
|
+
data.tar.gz: 37db580510184b407f7941e6c27ad00eb956fab8df6fb59a68406753c14813bca7007de9da53ca7cf9a858f456d40f2a5da2fdd872c44bdd33997b052132ba2c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.22 (2026-10-09)
|
|
4
|
+
|
|
5
|
+
- Work scheduled outside the app's own scheduler is watched too.
|
|
6
|
+
`config.recurring_jobs` declares it by the name it runs under (a job
|
|
7
|
+
class, `"rake <task>"`, or a `DeployAngel.task` name) with a cron line
|
|
8
|
+
or Fugit's words, for cron, Heroku Scheduler, or a Kubernetes CronJob.
|
|
9
|
+
- `DeployAngel.task("name") { ... }` records plain code that a scheduler
|
|
10
|
+
starts as a run of that name, with its duration and whether it raised.
|
|
11
|
+
- Rake tasks a schedule names are recorded with no code change, when the
|
|
12
|
+
task loads the app (`:environment`).
|
|
13
|
+
- The whenever gem's `config/schedule.rb` is read with whenever's own
|
|
14
|
+
parser, when whenever is in the bundle: rake tasks and runners that
|
|
15
|
+
perform a job class are expected on schedule, in the server's zone.
|
|
16
|
+
|
|
3
17
|
## 0.1.21 (2026-10-09)
|
|
4
18
|
|
|
5
19
|
- `deployangel exercise` stops requesting a page after its first 404, 405,
|
data/README.md
CHANGED
|
@@ -210,6 +210,43 @@ end
|
|
|
210
210
|
Jobs that exist only in Redis, such as ones created in code or in the
|
|
211
211
|
Sidekiq web UI, aren't read.
|
|
212
212
|
|
|
213
|
+
#### Cron, Heroku Scheduler, and whenever
|
|
214
|
+
|
|
215
|
+
Work that something outside the app starts on a schedule, such as cron,
|
|
216
|
+
Heroku Scheduler, or a Kubernetes CronJob, is invisible to those files.
|
|
217
|
+
Declare it by the name it runs under, with a cron line or words like
|
|
218
|
+
`"every day at 4am"`, in the server's time zone unless you add one
|
|
219
|
+
(`"0 3 * * * America/New_York"`):
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
DeployAngel.configure do |config|
|
|
223
|
+
config.recurring_jobs = {
|
|
224
|
+
"NightlyInvoiceJob" => "0 3 * * *", # bin/rails runner "NightlyInvoiceJob.perform_now"
|
|
225
|
+
"rake invoices:send" => "every day at 4am", # bin/rails invoices:send
|
|
226
|
+
"nightly import" => "30 2 * * *" # DeployAngel.task("nightly import") { ... }
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- **A job class** is recorded wherever it runs, a one-off `rails runner`
|
|
232
|
+
included.
|
|
233
|
+
- **A rake task** named `"rake <task>"` is recorded with no code change, as
|
|
234
|
+
long as it loads the app (depends on `:environment`).
|
|
235
|
+
- **Any other code** is recorded when you wrap it, which returns the block's
|
|
236
|
+
value and re-raises what it raises:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
DeployAngel.task("nightly import") { Importer.run }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The [whenever](https://github.com/javan/whenever) gem's `config/schedule.rb`
|
|
243
|
+
is read automatically when whenever is in the app's bundle (it's usually in
|
|
244
|
+
the Gemfile already, for deploys): its `rake` jobs, and `runner` jobs that
|
|
245
|
+
perform a job class (`"NightlyInvoiceJob.perform_later"`), are expected on
|
|
246
|
+
the schedule whenever writes to the crontab. Other `runner` code and
|
|
247
|
+
`command` jobs can't be matched to a run; wrap them in `DeployAngel.task`
|
|
248
|
+
and declare them as above.
|
|
249
|
+
|
|
213
250
|
## Checkpoints
|
|
214
251
|
|
|
215
252
|
Errors and latency don't catch work that silently stops happening. Count the
|
|
@@ -11,7 +11,8 @@ 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, :ignored_routes, :sidekiq_cron_schedule_file, :exception_messages
|
|
14
|
+
:file_digests, :critical_flows, :ignored_routes, :sidekiq_cron_schedule_file, :exception_messages,
|
|
15
|
+
:recurring_jobs
|
|
15
16
|
|
|
16
17
|
def initialize(env = ENV)
|
|
17
18
|
@token = env["DEPLOYANGEL_TOKEN"]
|
|
@@ -36,6 +37,12 @@ module DeployAngel
|
|
|
36
37
|
# For an app that loads sidekiq-cron's jobs itself (load_from_hash!)
|
|
37
38
|
# from its own file, such as "config/sidekiq_schedule.yml.erb".
|
|
38
39
|
@sidekiq_cron_schedule_file = nil
|
|
40
|
+
# Work scheduled outside the app's own scheduler (cron, Heroku
|
|
41
|
+
# Scheduler, a Kubernetes CronJob), by the name it runs under: a job
|
|
42
|
+
# class, "rake <task>", or a DeployAngel.task name. Each schedule is a
|
|
43
|
+
# cron line or Fugit's words ("every day at 4am"), optionally ending
|
|
44
|
+
# in a time zone.
|
|
45
|
+
@recurring_jobs = {}
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
# Reports only with a token, and by default in every environment but
|
data/lib/deployangel/metadata.rb
CHANGED
|
@@ -63,7 +63,73 @@ module DeployAngel
|
|
|
63
63
|
# Declared recurring jobs. Each source is read on its own, so a file that
|
|
64
64
|
# can't be read leaves the others' schedules in place.
|
|
65
65
|
def schedules
|
|
66
|
-
sidekiq_cron_schedules + sidekiq_scheduler_schedules
|
|
66
|
+
sidekiq_cron_schedules + sidekiq_scheduler_schedules + whenever_schedules + configured_schedules
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# config.recurring_jobs: work scheduled outside the app, by the name it
|
|
70
|
+
# runs under. A schedule without a zone is read in the server's.
|
|
71
|
+
def configured_schedules
|
|
72
|
+
Array(@config.recurring_jobs).filter_map do |name, schedule|
|
|
73
|
+
next if name.to_s.strip.empty? || schedule.to_s.strip.empty?
|
|
74
|
+
|
|
75
|
+
{ "key" => name.to_s, "class" => name.to_s, "schedule" => schedule.to_s, "source" => "config", "time_zone" => local_time_zone }
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError
|
|
78
|
+
[]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
WHENEVER_FILE = "config/schedule.rb"
|
|
82
|
+
WHENEVER_JOB = /\A\s*(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)(?:\.set\(.*?\))?\.(?:perform_now|perform_later|perform_async|perform_inline)\b/m
|
|
83
|
+
|
|
84
|
+
# The whenever gem's config/schedule.rb, which it turns into the server's
|
|
85
|
+
# crontab on deploy. Read with whenever's own parser, so times come out
|
|
86
|
+
# as cron reads them, in the server's zone. Rake tasks are recorded by
|
|
87
|
+
# name (Tasks.install_rake); a runner that performs a job class is
|
|
88
|
+
# matched by that class. Other commands can't be told apart, so they're
|
|
89
|
+
# left to config.recurring_jobs and DeployAngel.task.
|
|
90
|
+
def whenever_schedules
|
|
91
|
+
path = File.join(@root, WHENEVER_FILE)
|
|
92
|
+
return [] unless File.file?(path) && whenever_loaded?
|
|
93
|
+
|
|
94
|
+
list = ::Whenever::JobList.new(file: path)
|
|
95
|
+
chronic = list.instance_variable_get(:@chronic_options) || {}
|
|
96
|
+
by_time = (list.instance_variable_get(:@jobs) || {}).values.flat_map(&:to_a)
|
|
97
|
+
by_time.flat_map do |time, jobs|
|
|
98
|
+
Array(jobs).flat_map { |job| whenever_entries(time, job, chronic) }
|
|
99
|
+
end.uniq { |schedule| schedule["key"] }
|
|
100
|
+
rescue StandardError, ScriptError
|
|
101
|
+
[]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def whenever_loaded?
|
|
105
|
+
require "whenever"
|
|
106
|
+
true
|
|
107
|
+
rescue LoadError
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def whenever_entries(time, job, chronic)
|
|
112
|
+
name = whenever_job_name(job) or return []
|
|
113
|
+
crons = ::Whenever::Output::Cron.enumerate(time).flat_map do |each|
|
|
114
|
+
::Whenever::Output::Cron.enumerate(job.at, false).map do |at|
|
|
115
|
+
::Whenever::Output::Cron.new(each, nil, at, chronic_options: chronic).time_in_cron_syntax.to_s
|
|
116
|
+
end
|
|
117
|
+
end.uniq.reject { |cron| cron.empty? || cron == "@reboot" }
|
|
118
|
+
crons.map do |cron|
|
|
119
|
+
{ "key" => crons.one? ? name : "#{name} (#{cron})", "class" => name, "schedule" => cron, "source" => "whenever",
|
|
120
|
+
"time_zone" => local_time_zone }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def whenever_job_name(job)
|
|
125
|
+
template = job.instance_variable_get(:@template).to_s
|
|
126
|
+
task = job.instance_variable_get(:@options)&.dig(:task).to_s.strip
|
|
127
|
+
if template.match?(/\brake :task\b/)
|
|
128
|
+
rake = task.split.first.to_s
|
|
129
|
+
"#{Tasks::RAKE_PREFIX}#{rake}" unless rake.empty?
|
|
130
|
+
elsif template.include?(":runner_command")
|
|
131
|
+
task[WHENEVER_JOB, 1]
|
|
132
|
+
end
|
|
67
133
|
end
|
|
68
134
|
|
|
69
135
|
# sidekiq-cron jobs from its schedule file (config/schedule.yml unless
|
|
@@ -24,14 +24,24 @@ module DeployAngel
|
|
|
24
24
|
DeployAngel::Apartment.install if DeployAngel.configuration.exception_messages
|
|
25
25
|
# The metadata goes in at start, before the reporter thread can run:
|
|
26
26
|
# the code fingerprint is built from its file digests.
|
|
27
|
+
metadata = DeployAngel::Rails::Metadata.new(app: app, config: DeployAngel.configuration, root: ::Rails.root.to_s, environment: ::Rails.env)
|
|
27
28
|
DeployAngel.start(
|
|
28
29
|
environment: ::Rails.env,
|
|
29
30
|
root: ::Rails.root.to_s,
|
|
30
31
|
framework: "rails",
|
|
31
32
|
framework_version: ::Rails.version,
|
|
32
33
|
logger: ::Rails.logger,
|
|
33
|
-
metadata:
|
|
34
|
+
metadata: metadata
|
|
34
35
|
)
|
|
36
|
+
# A rake task cron starts loads the app (its :environment
|
|
37
|
+
# prerequisite) before it runs, so the hook is in place in time.
|
|
38
|
+
DeployAngel::Tasks.install_rake(metadata.schedules) if DeployAngel.recording? && Railtie.rake_process?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.rake_process?
|
|
42
|
+
defined?(::Rake.application) && ::Rake.application.top_level_tasks.any?
|
|
43
|
+
rescue StandardError
|
|
44
|
+
false
|
|
35
45
|
end
|
|
36
46
|
end
|
|
37
47
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module DeployAngel
|
|
6
|
+
# Work that runs on a schedule but isn't a job, such as a rake task that
|
|
7
|
+
# cron, Heroku Scheduler, or whenever starts. Recorded as a job run under
|
|
8
|
+
# its name, with its duration and whether it raised, so DeployAngel can
|
|
9
|
+
# expect it on schedule like any recurring job.
|
|
10
|
+
module Tasks
|
|
11
|
+
NAME = /\A[[:graph:]][[:print:]]{0,199}\z/
|
|
12
|
+
RAKE_PREFIX = "rake "
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def run(name)
|
|
17
|
+
name = name.to_s
|
|
18
|
+
return yield unless DeployAngel.recording? && valid?(name)
|
|
19
|
+
|
|
20
|
+
DeployAngel.add_capability("jobs")
|
|
21
|
+
previous = ExecutionContext.enter(ExecutionContext::JOB)
|
|
22
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
|
+
failed = false
|
|
24
|
+
begin
|
|
25
|
+
yield
|
|
26
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised untouched
|
|
27
|
+
failed = true
|
|
28
|
+
DeployAngel.record_exception(e, source: "job_class:#{name}")
|
|
29
|
+
raise
|
|
30
|
+
ensure
|
|
31
|
+
ExecutionContext.restore(previous)
|
|
32
|
+
DeployAngel.record_job(job_class: name, duration_ms: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0,
|
|
33
|
+
failed: failed)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def valid?(name)
|
|
38
|
+
return true if name.match?(NAME)
|
|
39
|
+
|
|
40
|
+
unless @warned
|
|
41
|
+
DeployAngel.configuration.logger&.warn("DeployAngel didn't record task #{name.inspect}: use a name of up to 200 printable characters")
|
|
42
|
+
@warned = true
|
|
43
|
+
end
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Records the rake tasks a schedule names ("rake invoices:send"), so a
|
|
48
|
+
# task cron starts needs no code change. Only those tasks: others, like
|
|
49
|
+
# db:migrate at deploy time, aren't recurring work.
|
|
50
|
+
def install_rake(schedules)
|
|
51
|
+
return unless defined?(::Rake::Task)
|
|
52
|
+
|
|
53
|
+
names = Array(schedules).filter_map { |schedule| schedule["class"].to_s.delete_prefix(RAKE_PREFIX) if schedule["class"].to_s.start_with?(RAKE_PREFIX) }
|
|
54
|
+
return if names.empty?
|
|
55
|
+
|
|
56
|
+
@rake_tasks = names.to_set
|
|
57
|
+
::Rake::Task.prepend(RakeTask) unless ::Rake::Task <= RakeTask
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def scheduled_rake_task?(name)
|
|
63
|
+
@rake_tasks&.include?(name) || false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
module RakeTask
|
|
67
|
+
def execute(args = nil)
|
|
68
|
+
return super unless DeployAngel::Tasks.scheduled_rake_task?(name)
|
|
69
|
+
|
|
70
|
+
DeployAngel::Tasks.run("#{DeployAngel::Tasks::RAKE_PREFIX}#{name}") { super }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/deployangel/version.rb
CHANGED
data/lib/deployangel.rb
CHANGED
|
@@ -61,6 +61,15 @@ module DeployAngel
|
|
|
61
61
|
agent&.record_exception(exception, **options)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Records a block as a run of scheduled work, for code that cron or a
|
|
65
|
+
# scheduler outside the app starts and that isn't a job:
|
|
66
|
+
# DeployAngel.task("invoices:send") { Invoice.send_due }
|
|
67
|
+
# It returns the block's value and re-raises what it raises. Declare the
|
|
68
|
+
# schedule in config.recurring_jobs so a missed run is noticed.
|
|
69
|
+
def task(name, &)
|
|
70
|
+
Tasks.run(name, &)
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
CHECKPOINT_NAME = /\A[a-z0-9][a-z0-9_.:-]{0,99}\z/i
|
|
65
74
|
|
|
66
75
|
# Counts a business event, such as DeployAngel.checkpoint("order.fulfilled").
|
|
@@ -116,6 +125,7 @@ require_relative "deployangel/rack/http"
|
|
|
116
125
|
require_relative "deployangel/rails/http"
|
|
117
126
|
require_relative "deployangel/rails/active_job"
|
|
118
127
|
require_relative "deployangel/rails/error_subscriber"
|
|
128
|
+
require_relative "deployangel/tasks"
|
|
119
129
|
require_relative "deployangel/metadata"
|
|
120
130
|
require_relative "deployangel/rails/metadata"
|
|
121
131
|
require_relative "deployangel/sidekiq"
|
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.
|
|
4
|
+
version: 0.1.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Owens
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/deployangel/rails/metadata.rb
|
|
58
58
|
- lib/deployangel/rails/railtie.rb
|
|
59
59
|
- lib/deployangel/sidekiq.rb
|
|
60
|
+
- lib/deployangel/tasks.rb
|
|
60
61
|
- lib/deployangel/verification_waiter.rb
|
|
61
62
|
- lib/deployangel/version.rb
|
|
62
63
|
homepage: https://www.deployangel.com
|
|
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
84
|
- !ruby/object:Gem::Version
|
|
84
85
|
version: '0'
|
|
85
86
|
requirements: []
|
|
86
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.0
|
|
87
88
|
specification_version: 4
|
|
88
89
|
summary: 'DeployAngel agent for Rails: production verification for every deployment.'
|
|
89
90
|
test_files: []
|