deployangel 0.1.20 → 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 +20 -0
- data/README.md +40 -2
- data/lib/deployangel/configuration.rb +8 -1
- data/lib/deployangel/exerciser.rb +40 -16
- 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,25 @@
|
|
|
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
|
+
|
|
17
|
+
## 0.1.21 (2026-10-09)
|
|
18
|
+
|
|
19
|
+
- `deployangel exercise` stops requesting a page after its first 404, 405,
|
|
20
|
+
or 410, since a route table can list pages an app doesn't serve, and sends
|
|
21
|
+
that page's share to the pages that answered.
|
|
22
|
+
|
|
3
23
|
## 0.1.20 (2026-10-09)
|
|
4
24
|
|
|
5
25
|
- `deployangel exercise --url=<production URL>` sends a release's exercise
|
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
|
|
@@ -471,8 +508,9 @@ path parameters, spreading any request shortfall across them (at most 200
|
|
|
471
508
|
requests, about 5 a second, as `DeployAngel-Exercise`), then records what it
|
|
472
509
|
sent on the release, where the page lists it under "Exercised from your
|
|
473
510
|
side". It skips and names routes that change data or need a path
|
|
474
|
-
parameter
|
|
475
|
-
|
|
511
|
+
parameter, and stops requesting a page after its first 404, sending its
|
|
512
|
+
share to the pages that answered. A "CLI & coding agents" token can run
|
|
513
|
+
it. `--dry-run` shows what it would send.
|
|
476
514
|
|
|
477
515
|
### Suggested instructions for your coding agent
|
|
478
516
|
|
|
@@ -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
|
|
@@ -16,6 +16,9 @@ module DeployAngel
|
|
|
16
16
|
TIMEOUT = 10
|
|
17
17
|
# Stop when the app isn't answering, rather than send the rest into it.
|
|
18
18
|
MAX_CONSECUTIVE_ERRORS = 5
|
|
19
|
+
# A route table can list pages an app doesn't serve, like the edit page
|
|
20
|
+
# of a resource that has none. One answer like this is enough to know.
|
|
21
|
+
NOT_SERVED = [ 404, 405, 410 ].freeze
|
|
19
22
|
# Rails' :id and *path, Django's <int:pk>, FastAPI's {id}.
|
|
20
23
|
PARAMETER = /\/[:*{<]/
|
|
21
24
|
|
|
@@ -51,27 +54,34 @@ module DeployAngel
|
|
|
51
54
|
[ targets, skipped ]
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
# A route whose first request says it isn't served gets no more; its
|
|
58
|
+
# share goes to the routes that answered.
|
|
54
59
|
def run(exercise_plan)
|
|
55
60
|
targets, skipped = plan(exercise_plan)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
@statuses = Hash.new { |hash, key| hash[key] = Hash.new(0) }
|
|
62
|
+
@sent = 0
|
|
63
|
+
@errors_in_a_row = 0
|
|
64
|
+
leftover = 0
|
|
65
|
+
answered = []
|
|
59
66
|
targets.each do |target|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
statuses[kind] += 1
|
|
69
|
-
errors_in_a_row = kind == "error" ? errors_in_a_row + 1 : 0
|
|
67
|
+
target.count.times do |i|
|
|
68
|
+
break if unreachable?
|
|
69
|
+
|
|
70
|
+
status = send_request(target)
|
|
71
|
+
if i.zero? && NOT_SERVED.include?(status)
|
|
72
|
+
leftover += target.count - 1
|
|
73
|
+
break
|
|
74
|
+
end
|
|
70
75
|
end
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
answered << target if @statuses[target.key].keys.intersect?(%w[2xx 3xx])
|
|
77
|
+
end
|
|
78
|
+
leftover.times { |i| send_request(answered[i % answered.size]) unless unreachable? } if answered.any?
|
|
79
|
+
|
|
80
|
+
routes = targets.filter_map do |target|
|
|
81
|
+
statuses = @statuses.fetch(target.key, nil) or next
|
|
82
|
+
{ "key" => target.key, "requests" => statuses.values.sum, "statuses" => statuses.to_h }
|
|
73
83
|
end
|
|
74
|
-
Result.new(routes: routes, skipped: skipped, sent: sent, unreachable:
|
|
84
|
+
Result.new(routes: routes, skipped: skipped, sent: @sent, unreachable: unreachable?)
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
def url_for(path)
|
|
@@ -82,6 +92,20 @@ module DeployAngel
|
|
|
82
92
|
end
|
|
83
93
|
|
|
84
94
|
private
|
|
95
|
+
def send_request(target)
|
|
96
|
+
@sleeper.call(INTERVAL) if @sent.positive?
|
|
97
|
+
status = @requester.call(url_for(target.path))
|
|
98
|
+
@sent += 1
|
|
99
|
+
kind = status ? "#{status / 100}xx" : "error"
|
|
100
|
+
@statuses[target.key][kind] += 1
|
|
101
|
+
@errors_in_a_row = kind == "error" ? @errors_in_a_row + 1 : 0
|
|
102
|
+
status
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unreachable?
|
|
106
|
+
@errors_in_a_row >= MAX_CONSECUTIVE_ERRORS
|
|
107
|
+
end
|
|
108
|
+
|
|
85
109
|
# Requests the release is short of overall, spread across the routes,
|
|
86
110
|
# or sent to the home page when the plan names no route to send them to.
|
|
87
111
|
def spread_shortfall(targets, requests)
|
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: []
|