deployangel 0.1.23 → 0.1.25
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 +18 -0
- data/README.md +14 -1
- data/lib/deployangel/cli/formatter.rb +3 -1
- data/lib/deployangel/cli.rb +37 -2
- data/lib/deployangel/client.rb +1 -0
- data/lib/deployangel/doctor.rb +237 -0
- data/lib/deployangel/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05e4cde0e2b605e294d51c73f25af3a2b5d9496c6a8e0b3270c358a9a5b9e4ec
|
|
4
|
+
data.tar.gz: 722541542ff63d2cb8eedb0020a974a71ecf4eaaff3338e50e1002bc88e7ebbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9674f14d5c5f09540e2fe9a102fd1e16571c6492214a47b70a155bb6c70c98af1e333c58734b8eca2079aab323aca6090a47bf8b964111a59e6734dcee316d3
|
|
7
|
+
data.tar.gz: 71fad66b1e6f4d36b1794e8f403b05524a0b151a2b4232067e39f3f7341c80d6673b28362215bdf2250ef179640124249c397ec366c85556a9eae5f9d87ce10a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.25 (2026-10-10)
|
|
4
|
+
|
|
5
|
+
- `verify` prints what a clearance doesn't cover right under it: app-wide
|
|
6
|
+
signals without enough data, critical flows and changed items not run
|
|
7
|
+
since the deploy, and latency that rose but stayed within limits (from
|
|
8
|
+
the verdict document's new `clearance.scope`). The coding-agent
|
|
9
|
+
instructions `install agents` writes now say to report it too.
|
|
10
|
+
|
|
11
|
+
## 0.1.24 (2026-10-10)
|
|
12
|
+
|
|
13
|
+
- `deployangel doctor` checks the setup: the agent in the bundle, the
|
|
14
|
+
schedules in this checkout's files, and how production will know the
|
|
15
|
+
release; then, with a token, what DeployAngel has seen (processes
|
|
16
|
+
reporting and their releases, what the agent reported, and setup
|
|
17
|
+
problems with fixes). Only errors fail it. Run where the app runs, it
|
|
18
|
+
uses the agent's `DEPLOYANGEL_TOKEN` and shows the release that process
|
|
19
|
+
reports. Needs a DeployAngel server with `GET /api/v1/setup`.
|
|
20
|
+
|
|
3
21
|
## 0.1.23 (2026-10-09)
|
|
4
22
|
|
|
5
23
|
- GoodJob's cron (`config.good_job.cron`, or `GOOD_JOB_CRON`) is read from
|
data/README.md
CHANGED
|
@@ -441,8 +441,20 @@ bundle exec deployangel exercise --url=https://example.com # send the plan's re
|
|
|
441
441
|
bundle exec deployangel release --commit=$SHA # register a deploy (manual or CI)
|
|
442
442
|
bundle exec deployangel check --name="smoke: signup" --status=pass --covers=registration
|
|
443
443
|
bundle exec deployangel exception <fingerprint>
|
|
444
|
+
bundle exec deployangel doctor # check the setup
|
|
444
445
|
```
|
|
445
446
|
|
|
447
|
+
`deployangel doctor` checks this checkout (the agent in the bundle, the
|
|
448
|
+
schedules in its files, how production will know the release) and, with a
|
|
449
|
+
token, what DeployAngel has seen of the app: which processes report and the
|
|
450
|
+
release each runs, the routes, job classes, and scheduled jobs the agent
|
|
451
|
+
reported, and setup problems with what to do about each. Only errors (a
|
|
452
|
+
rejected token, an unreachable server, no agent in the bundle) exit 5;
|
|
453
|
+
warnings exit 0, so it's safe in CI. It reads `DEPLOYANGEL_API_TOKEN`, or
|
|
454
|
+
`DEPLOYANGEL_TOKEN` where the app runs, so
|
|
455
|
+
`heroku run bundle exec deployangel doctor` also shows the release that
|
|
456
|
+
process reports.
|
|
457
|
+
|
|
446
458
|
Output is text on a terminal and JSON (the verdict document) when piped, or
|
|
447
459
|
choose with `--format=text|json`. Exit codes:
|
|
448
460
|
|
|
@@ -533,7 +545,8 @@ verdict: call the `wait_for_verification` MCP tool with the commit and
|
|
|
533
545
|
`until: "initial"`, calling again while it's in progress, or run
|
|
534
546
|
`bundle exec deployangel verify --commit=<sha> --wait --until=initial`.
|
|
535
547
|
|
|
536
|
-
- Exit 0 / verified: the release is cleared. Report the clearance line
|
|
548
|
+
- Exit 0 / verified: the release is cleared. Report the clearance line, what
|
|
549
|
+
it says it doesn't cover ("Not assessed", "Not run since the deploy"), and
|
|
537
550
|
anything DeployAngel is still watching, then move on.
|
|
538
551
|
- Exit 6: no problems so far, but NOT cleared. Report "no problems so far, not
|
|
539
552
|
yet cleared" and the expected clearance time. DeployAngel keeps verifying and
|
|
@@ -70,7 +70,9 @@ module DeployAngel
|
|
|
70
70
|
|
|
71
71
|
def notes(document)
|
|
72
72
|
v = document["verification"] || {}
|
|
73
|
-
|
|
73
|
+
# What a clearance doesn't cover, right under it (servers from
|
|
74
|
+
# 2026-10-10 send it).
|
|
75
|
+
notes = Array(document.dig("clearance", "scope", "lines"))
|
|
74
76
|
if (check = v["initial_check"]) && v["verdict"].nil?
|
|
75
77
|
notes << "Initial check: #{check["result"].tr("_", " ")}. Not cleared yet."
|
|
76
78
|
end
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -12,13 +12,14 @@ require_relative "ci_environment"
|
|
|
12
12
|
require_relative "verification_waiter"
|
|
13
13
|
require_relative "cli/formatter"
|
|
14
14
|
require_relative "exerciser"
|
|
15
|
+
require_relative "doctor"
|
|
15
16
|
|
|
16
17
|
module DeployAngel
|
|
17
18
|
# `deployangel` command for developers, CI, and coding agents.
|
|
18
19
|
# Runs without booting Rails.
|
|
19
20
|
class CLI
|
|
20
21
|
USAGE_ERROR = 5
|
|
21
|
-
COMMANDS = %w[release verify status plan exercise exception check install mcp version help].freeze
|
|
22
|
+
COMMANDS = %w[release verify status plan exercise exception check install doctor mcp version help].freeze
|
|
22
23
|
|
|
23
24
|
HELP = <<~TEXT
|
|
24
25
|
Usage: deployangel <command> [options]
|
|
@@ -39,6 +40,8 @@ module DeployAngel
|
|
|
39
40
|
install Add a Kamal deploy hook, deployangel install kamal
|
|
40
41
|
bake the commit into an image, deployangel install docker
|
|
41
42
|
or set up coding agents deployangel install agents
|
|
43
|
+
doctor Check the setup: this checkout [--format=text|json]
|
|
44
|
+
and what DeployAngel has seen
|
|
42
45
|
mcp Run the MCP server over stdio (for coding agents)
|
|
43
46
|
|
|
44
47
|
With no target, verify, plan, and check use the current git HEAD commit.
|
|
@@ -88,7 +91,8 @@ module DeployAngel
|
|
|
88
91
|
`until: "initial"`, calling again while it's in progress, or run
|
|
89
92
|
`bundle exec deployangel verify --commit=<sha> --wait --until=initial`.
|
|
90
93
|
|
|
91
|
-
- Exit 0 / verified: the release is cleared. Report the clearance line
|
|
94
|
+
- Exit 0 / verified: the release is cleared. Report the clearance line, what
|
|
95
|
+
it says it doesn't cover ("Not assessed", "Not run since the deploy"), and
|
|
92
96
|
anything DeployAngel is still watching, then move on.
|
|
93
97
|
- Exit 6: no problems so far, but NOT cleared. Report "no problems so far, not
|
|
94
98
|
yet cleared" and the expected clearance time. DeployAngel keeps verifying and
|
|
@@ -148,6 +152,7 @@ module DeployAngel
|
|
|
148
152
|
when "exception" then exception
|
|
149
153
|
when "check" then check
|
|
150
154
|
when "install" then install
|
|
155
|
+
when "doctor" then doctor
|
|
151
156
|
when "mcp" then mcp
|
|
152
157
|
when "version", "--version", "-v" then @stdout.puts(DeployAngel::VERSION) || 0
|
|
153
158
|
when nil, "help", "--help", "-h" then @stdout.puts(HELP) || 0
|
|
@@ -323,6 +328,36 @@ module DeployAngel
|
|
|
323
328
|
0
|
|
324
329
|
end
|
|
325
330
|
|
|
331
|
+
# What this checkout will report and what DeployAngel has seen of the
|
|
332
|
+
# app. Warnings are advice, so only errors (a rejected token, an
|
|
333
|
+
# unreachable server, no agent in the bundle) fail it.
|
|
334
|
+
def doctor
|
|
335
|
+
format = parse(format: nil) { |o, opts| o.on("--format=FORMAT", %w[text json]) { |v| opts[:format] = v } }[:format]
|
|
336
|
+
doctor = Doctor.new(root: @root, client: doctor_client, env: @env)
|
|
337
|
+
output(doctor.to_h, format) { doctor_text(doctor.checks) }
|
|
338
|
+
doctor.failed? ? USAGE_ERROR : 0
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
DOCTOR_MARKS = { ok: "✓", warn: "!", error: "✗", info: "·" }.freeze
|
|
342
|
+
|
|
343
|
+
def doctor_text(checks)
|
|
344
|
+
lines = [ "DeployAngel doctor" ]
|
|
345
|
+
checks.each do |check|
|
|
346
|
+
lines << " #{DOCTOR_MARKS.fetch(check.status)} #{check.message}"
|
|
347
|
+
lines << " #{check.fix}" if check.fix
|
|
348
|
+
end
|
|
349
|
+
lines.join("\n")
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Either token works: the setup endpoint answers any of the app's. The
|
|
353
|
+
# agent's own token is for running it where the app runs.
|
|
354
|
+
def doctor_client
|
|
355
|
+
return @client if @client
|
|
356
|
+
|
|
357
|
+
token = [ @env["DEPLOYANGEL_API_TOKEN"], @env["DEPLOYANGEL_TOKEN"] ].find { |value| !value.to_s.empty? } or return
|
|
358
|
+
Client.new(token: token, endpoint: @env["DEPLOYANGEL_URL"] || Configuration::DEFAULT_ENDPOINT)
|
|
359
|
+
end
|
|
360
|
+
|
|
326
361
|
def install
|
|
327
362
|
case (target = @argv.shift)
|
|
328
363
|
when "kamal" then install_kamal
|
data/lib/deployangel/client.rb
CHANGED
|
@@ -31,6 +31,7 @@ module DeployAngel
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def token_info = get("/api/v1/token")
|
|
34
|
+
def setup = get("/api/v1/setup")
|
|
34
35
|
def latest_deployment = get("/api/v1/deployments/latest")
|
|
35
36
|
def deployments(**filters) = get("/api/v1/deployments", filters.compact)["deployments"]
|
|
36
37
|
def exception(fingerprint) = get("/api/v1/exceptions/#{URI.encode_www_form_component(fingerprint)}")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "configuration"
|
|
5
|
+
require_relative "metadata"
|
|
6
|
+
require_relative "rails/metadata"
|
|
7
|
+
require_relative "core/release"
|
|
8
|
+
|
|
9
|
+
module DeployAngel
|
|
10
|
+
# `deployangel doctor`: what this checkout will report, and what
|
|
11
|
+
# DeployAngel has seen of the app so far (GET /api/v1/setup), as checks
|
|
12
|
+
# with what to do about each. It runs without booting the app, so it
|
|
13
|
+
# reads only the schedules kept in files.
|
|
14
|
+
class Doctor
|
|
15
|
+
# status is ok, warn, error, or info. Only errors fail the command.
|
|
16
|
+
Check = Struct.new(:status, :message, :fix, keyword_init: true) do
|
|
17
|
+
def to_h = { "status" => status.to_s, "message" => message, "fix" => fix }.compact
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
SOURCES = { "solid_queue" => "Solid Queue", "sidekiq_cron" => "sidekiq-cron", "sidekiq_scheduler" => "sidekiq-scheduler",
|
|
21
|
+
"whenever" => "whenever", "good_job" => "GoodJob", "config" => "agent config" }.freeze
|
|
22
|
+
TOKEN_HELP = "Set DEPLOYANGEL_API_TOKEN (a \"CLI & coding agents\" token from the app's Settings) or DEPLOYANGEL_TOKEN " \
|
|
23
|
+
"(the app's agent token), then run this again."
|
|
24
|
+
MAX_HOSTS = 5
|
|
25
|
+
|
|
26
|
+
attr_reader :setup
|
|
27
|
+
|
|
28
|
+
# client is nil when no token is set; the local checks still run.
|
|
29
|
+
def initialize(root:, client:, env: {})
|
|
30
|
+
@root = root
|
|
31
|
+
@client = client
|
|
32
|
+
@env = env
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def checks
|
|
36
|
+
@checks ||= begin
|
|
37
|
+
@setup, server = server_checks
|
|
38
|
+
[ *agent_checks, here_check, schedule_check, *server, release_check, *comparison_checks ].compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def failed?
|
|
43
|
+
checks.any? { |check| check.status == :error }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_h
|
|
47
|
+
{ "checks" => checks.map(&:to_h), "setup" => setup }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
def agent_checks
|
|
52
|
+
lock = read("Gemfile.lock") or return []
|
|
53
|
+
version = lock[/^ deployangel \(([^)]+)\)/, 1]
|
|
54
|
+
return [ Check.new(status: :ok, message: "Agent: deployangel #{version} in Gemfile.lock") ] if version
|
|
55
|
+
|
|
56
|
+
[ Check.new(status: :error, message: "The agent isn't in the bundle", fix: "Add gem \"deployangel\" to the Gemfile and run bundle install.") ]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def schedule_check
|
|
60
|
+
counts = local_schedules.group_by { |schedule| schedule["source"] }.map { |source, list| "#{SOURCES.fetch(source, source)} #{list.size}" }
|
|
61
|
+
later = [ ("GoodJob's cron" if read("Gemfile.lock").to_s.include?(" good_job (")), "config.recurring_jobs" ].compact
|
|
62
|
+
note = "#{later.join(" and ")} #{later.one? ? "is" : "are"} read when the app runs."
|
|
63
|
+
if counts.any?
|
|
64
|
+
Check.new(status: :ok, message: "#{count(local_schedules.size, "scheduled job")} in this checkout (#{counts.join(", ")}). #{note}")
|
|
65
|
+
else
|
|
66
|
+
Check.new(status: :info, message: "No scheduled jobs in this checkout's schedule files. #{note}")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The token's app and what DeployAngel has seen of it.
|
|
71
|
+
def server_checks
|
|
72
|
+
return [ nil, [ Check.new(status: :warn, message: "No token, so only this checkout was checked", fix: TOKEN_HELP) ] ] unless @client
|
|
73
|
+
|
|
74
|
+
setup = @client.setup
|
|
75
|
+
checks = [ app_check(setup), agent_check(setup), metadata_check(setup), *gap_checks(setup), progress_check(setup) ]
|
|
76
|
+
[ setup, checks.compact ]
|
|
77
|
+
rescue Client::Unauthorized => e
|
|
78
|
+
[ nil, [ Check.new(status: :error, message: "DeployAngel rejected the token (#{e.message})", fix: TOKEN_HELP) ] ]
|
|
79
|
+
rescue Client::NotFound
|
|
80
|
+
[ nil, [ Check.new(status: :warn, message: "This DeployAngel server doesn't support doctor yet; only this checkout was checked") ] ]
|
|
81
|
+
rescue Client::Error => e
|
|
82
|
+
[ nil, [ Check.new(status: :error, message: "Couldn't reach DeployAngel: #{e.message}") ] ]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def app_check(setup)
|
|
86
|
+
app = setup["app"] || {}
|
|
87
|
+
Check.new(status: :ok, message: "App: #{app["name"]} (#{app["environment"]})")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def agent_check(setup)
|
|
91
|
+
agent = setup["agent"] || {}
|
|
92
|
+
processes = Array(agent["processes"])
|
|
93
|
+
if agent["reporting"]
|
|
94
|
+
releases = processes.map { |process| process["release"] }.uniq
|
|
95
|
+
release = releases.compact.one? && releases.size == 1 ? ", release #{releases.first}" : ""
|
|
96
|
+
from = processes.any? ? " from #{hosts(processes)}" : ""
|
|
97
|
+
Check.new(status: :ok, message: "Agent reporting#{from}#{release}")
|
|
98
|
+
elsif agent["last_received_at"]
|
|
99
|
+
Check.new(status: :warn, message: "The agent last reported #{time(agent["last_received_at"])}, and nothing since",
|
|
100
|
+
fix: "Check the app is running, and that DEPLOYANGEL_TOKEN is still set on its processes.")
|
|
101
|
+
else
|
|
102
|
+
Check.new(status: :warn, message: "The agent hasn't reported yet",
|
|
103
|
+
fix: "Deploy with DEPLOYANGEL_TOKEN set to the app's agent token (Settings, Tokens: \"Deployed application\").")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def metadata_check(setup)
|
|
108
|
+
metadata = setup["metadata"] or return
|
|
109
|
+
jobs = Array(setup["scheduled_jobs"])
|
|
110
|
+
problems = jobs.select { |job| %w[overdue failed].include?(job["status"]) }
|
|
111
|
+
problem_text = problems.any? ? ", #{problems.size} needing attention (#{problems.first(3).map { |job| "#{job["label"]} #{job["status"]}" }.join(", ")})" : ""
|
|
112
|
+
Check.new(status: problems.any? ? :warn : :ok,
|
|
113
|
+
message: "Reported: #{count(metadata["routes"], "route")}, #{count(metadata["job_classes"], "job class", "job classes")}, " \
|
|
114
|
+
"#{count(metadata["schedules"], "scheduled job")}#{problem_text}",
|
|
115
|
+
fix: ("See them on the app page, under Scheduled jobs." if problems.any?))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def gap_checks(setup)
|
|
119
|
+
Array(setup["gaps"]).map { |gap| Check.new(status: :warn, message: gap["title"], fix: gap["fix"]) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def progress_check(setup)
|
|
123
|
+
deployments = setup["deployments"] || {}
|
|
124
|
+
latest = deployments["latest"]
|
|
125
|
+
message = if setup["warm_up_ends_at"]
|
|
126
|
+
"Learning what's normal for this app until #{time(setup["warm_up_ends_at"])}; deploys before then are checked, not cleared"
|
|
127
|
+
elsif latest
|
|
128
|
+
"#{count(deployments["count"], "deploy")} so far; the latest, #{latest["label"]}, is #{latest["verdict"] || latest["state"]}"
|
|
129
|
+
elsif setup.dig("agent", "last_received_at")
|
|
130
|
+
"No deploys yet: your next deploy gets the first verdict"
|
|
131
|
+
end
|
|
132
|
+
message && Check.new(status: :info, message: message)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Run where the app runs (`heroku run`, a container shell), the release
|
|
136
|
+
# this process would report. On a developer machine it would only be
|
|
137
|
+
# the checkout's git HEAD, which says nothing about production, so
|
|
138
|
+
# it's left out there.
|
|
139
|
+
def here_check
|
|
140
|
+
release = Release.resolve(config: Configuration.new(@env), env: @env, root: @root)
|
|
141
|
+
if release.unknown?
|
|
142
|
+
return unless where_the_app_runs?
|
|
143
|
+
|
|
144
|
+
Check.new(status: :warn, message: "Here, the agent can't tell which release is running",
|
|
145
|
+
fix: "Set DEPLOYANGEL_REVISION to the deployed commit, or see https://deployangel.com/docs#containers.")
|
|
146
|
+
elsif release.source != "git_head"
|
|
147
|
+
Check.new(status: :ok, message: "Here, the agent would report release #{release.version || release.commit} (from #{release.source})")
|
|
148
|
+
end
|
|
149
|
+
rescue StandardError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def where_the_app_runs?
|
|
154
|
+
!@env["DEPLOYANGEL_TOKEN"].to_s.empty? && @env["DEPLOYANGEL_API_TOKEN"].to_s.empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# How production will know the release, judged from what's reporting
|
|
158
|
+
# once anything is, and from this checkout's deploy files before then.
|
|
159
|
+
def release_check
|
|
160
|
+
return if Array(setup&.dig("agent", "processes")).any?
|
|
161
|
+
|
|
162
|
+
if file?("config/deploy.yml")
|
|
163
|
+
Check.new(status: :ok, message: "Kamal deploys: the agent reads the release from KAMAL_VERSION")
|
|
164
|
+
elsif file?("render.yaml")
|
|
165
|
+
Check.new(status: :ok, message: "Render deploys: the agent reads the commit from RENDER_GIT_COMMIT")
|
|
166
|
+
elsif read(".do/app.yaml").to_s.include?("DEPLOYANGEL_REVISION")
|
|
167
|
+
Check.new(status: :ok, message: "The DigitalOcean app spec passes the commit in DEPLOYANGEL_REVISION")
|
|
168
|
+
elsif (dockerfile = read("Dockerfile"))
|
|
169
|
+
if dockerfile.include?("DEPLOYANGEL_REVISION")
|
|
170
|
+
Check.new(status: :ok, message: "The Dockerfile passes the commit in DEPLOYANGEL_REVISION")
|
|
171
|
+
else
|
|
172
|
+
# Platforms can pass it another way, so this is advice until the
|
|
173
|
+
# agent reports: then the release it names settles it.
|
|
174
|
+
Check.new(status: :info, message: "The Dockerfile doesn't set DEPLOYANGEL_REVISION; unless your platform passes the commit, " \
|
|
175
|
+
"releases won't be named by commit",
|
|
176
|
+
fix: "Run `deployangel install docker`, then build with --build-arg GIT_SHA=$(git rev-parse HEAD).")
|
|
177
|
+
end
|
|
178
|
+
elsif file?("Capfile")
|
|
179
|
+
Check.new(status: :ok, message: "Capistrano deploys: the agent reads the REVISION file Capistrano writes")
|
|
180
|
+
elsif file?("Procfile") || file?("app.json")
|
|
181
|
+
Check.new(status: :info, message: "On Heroku, turn on dyno metadata so the agent knows the commit",
|
|
182
|
+
fix: "heroku labs:enable runtime-dyno-metadata && heroku labs:enable runtime-dyno-build-metadata")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Schedules in this checkout that the running agent hasn't reported:
|
|
187
|
+
# not deployed yet, or an agent too old to read them.
|
|
188
|
+
def comparison_checks
|
|
189
|
+
reported = setup&.dig("metadata", "schedules") or return []
|
|
190
|
+
return [] unless local_schedules.size > reported
|
|
191
|
+
|
|
192
|
+
[ Check.new(status: :warn, message: "This checkout declares #{count(local_schedules.size, "scheduled job")}; the running agent reported #{reported}",
|
|
193
|
+
fix: "Deploy this code, or update the agent: whenever schedules need deployangel 0.1.22 or later, GoodJob's 0.1.23.") ]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def local_schedules
|
|
197
|
+
@local_schedules ||= begin
|
|
198
|
+
config = Configuration.new({})
|
|
199
|
+
metadata = if file?("config/application.rb")
|
|
200
|
+
Rails::Metadata.new(app: nil, config: config, root: @root, environment: "production")
|
|
201
|
+
else
|
|
202
|
+
DeployAngel::Metadata.new(config: config, root: @root, environment: "production")
|
|
203
|
+
end
|
|
204
|
+
metadata.schedules
|
|
205
|
+
rescue StandardError, ScriptError
|
|
206
|
+
[]
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def hosts(processes)
|
|
211
|
+
names = processes.map { |process| process["processes"].to_i > 1 ? "#{process["host"]} ×#{process["processes"]}" : process["host"] }
|
|
212
|
+
total = processes.sum { |process| process["processes"].to_i }
|
|
213
|
+
listed = names.first(MAX_HOSTS).join(", ")
|
|
214
|
+
listed += ", and others" if names.size > MAX_HOSTS
|
|
215
|
+
"#{count(total, "process", "processes")} (#{listed})"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def time(iso)
|
|
219
|
+
Time.iso8601(iso).utc.strftime("%b %-d, %H:%M UTC")
|
|
220
|
+
rescue ArgumentError, TypeError
|
|
221
|
+
iso.to_s
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def count(number, singular, plural = "#{singular}s")
|
|
225
|
+
"#{number.to_i} #{number.to_i == 1 ? singular : plural}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def file?(relative) = File.file?(File.join(@root, relative))
|
|
229
|
+
|
|
230
|
+
def read(relative)
|
|
231
|
+
path = File.join(@root, relative)
|
|
232
|
+
File.read(path) if File.file?(path)
|
|
233
|
+
rescue SystemCallError
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
data/lib/deployangel/version.rb
CHANGED
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.25
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Owens
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/deployangel/core/redaction.rb
|
|
47
47
|
- lib/deployangel/core/release.rb
|
|
48
48
|
- lib/deployangel/core/transport.rb
|
|
49
|
+
- lib/deployangel/doctor.rb
|
|
49
50
|
- lib/deployangel/exerciser.rb
|
|
50
51
|
- lib/deployangel/fork_hook.rb
|
|
51
52
|
- lib/deployangel/mcp.rb
|