deployangel 0.1.15 → 0.1.17
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 +25 -0
- data/README.md +34 -11
- data/lib/deployangel/agent.rb +43 -5
- data/lib/deployangel/cli.rb +79 -5
- data/lib/deployangel/configuration.rb +10 -4
- data/lib/deployangel/core/release.rb +80 -1
- data/lib/deployangel/metadata.rb +4 -2
- data/lib/deployangel/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a288e6edf81d6e4119526b5ce13d87e033a9bc54d5a02679bf5d5585771e67fb
|
|
4
|
+
data.tar.gz: 0530abbe1a6dd018fbb528697e0a86fd7c10377ba09271b1bfd576ac2cca2e3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12968f549c284c8c96f5151455341f6966f1c4dc5ee2e3a8a634cbe0d751235c8868ec254a6e40a49376ab260497262107da682ab16f20f0a53c5446ebde6989
|
|
7
|
+
data.tar.gz: '01408cbd6f6fcb74ef21eb0b3f2db04ebf9c24981c7d798f897edca04487018ad84383216cb6744102b5e6eba1a51f6be5a6204b7757daab328176bca1f76f63'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.17 (2026-10-08)
|
|
4
|
+
|
|
5
|
+
- The agent reports from every Rails environment but `development` and `test`,
|
|
6
|
+
so a staging or preview app running with `RAILS_ENV=staging` works without
|
|
7
|
+
`DEPLOYANGEL_ENABLED=true`. `config.environments`, when set, still limits it to
|
|
8
|
+
the listed environments.
|
|
9
|
+
|
|
10
|
+
## 0.1.16 (2026-10-08)
|
|
11
|
+
|
|
12
|
+
- The agent finds the release in a git checkout when nothing earlier names
|
|
13
|
+
it: the commit `HEAD` names, read from `.git` in the app root or up to 3
|
|
14
|
+
directories above it (worktrees and submodules included), without running
|
|
15
|
+
`git` (source `git_head`). It comes after a `REVISION` file and before ECS.
|
|
16
|
+
- When nothing else identifies the release, the agent reports a fingerprint
|
|
17
|
+
of the app's code as the version (`code:` and 12 hex characters, source
|
|
18
|
+
`code_fingerprint`): the hash of the file digests it already computes. It's
|
|
19
|
+
worked out in the reporter thread as soon as it starts, never at boot or
|
|
20
|
+
in a request, and needs file digests on. A warning suggests setting
|
|
21
|
+
`DEPLOYANGEL_REVISION`: without a commit, DeployAngel shows which files
|
|
22
|
+
changed but not the commits and pull requests.
|
|
23
|
+
- `deployangel install docker` adds `ARG GIT_SHA` and
|
|
24
|
+
`ENV DEPLOYANGEL_REVISION=$GIT_SHA` at the end of the Dockerfile's last
|
|
25
|
+
stage, before its `CMD`, and says which `--build-arg` to pass. It never
|
|
26
|
+
edits CI workflows.
|
|
27
|
+
|
|
3
28
|
## 0.1.15 (2026-10-06)
|
|
4
29
|
|
|
5
30
|
- Checkpoints report whether they were recorded in an HTTP request or a job
|
data/README.md
CHANGED
|
@@ -36,26 +36,46 @@ The agent must know which release it is running. It finds it in this order:
|
|
|
36
36
|
didn't come from GitHub
|
|
37
37
|
7. Coolify: `SOURCE_COMMIT`. Dokku: `GIT_REV`
|
|
38
38
|
8. a `REVISION` file in the app root, which Capistrano writes and any build step can
|
|
39
|
-
9.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
9. a git checkout, for servers deployed by `git pull`, Fabric, or Ansible: the commit
|
|
40
|
+
`HEAD` names, read from `.git` in the app root or up to 3 directories above it
|
|
41
|
+
(worktrees and submodules included). The agent reads the files; it never runs `git`
|
|
42
|
+
10. ECS, including Fargate: the container's image, from the metadata endpoint ECS
|
|
43
|
+
provides (one local request at boot). The image tag is the release when it's a
|
|
44
|
+
commit or a version like `v1.4.2`; with a moving tag like `latest`, the image
|
|
45
|
+
digest is
|
|
46
|
+
11. a fingerprint of the app's code, reported as `code:` and 12 hex characters: the
|
|
47
|
+
hash of the [file digests](#what-it-sends) the agent already computes, so the
|
|
48
|
+
same code is the same release. It needs file digests on, and is worked out in
|
|
49
|
+
the background when the reporter starts, not at boot. Without a commit, the
|
|
50
|
+
dashboard shows which files changed but not the commits and pull requests, so
|
|
51
|
+
the agent logs a warning suggesting `DEPLOYANGEL_REVISION`
|
|
43
52
|
|
|
44
53
|
For other Docker deploys (compose, Swarm, Kubernetes), and on ECS to see each
|
|
45
54
|
release by its commit, bake the commit into the image, since `.dockerignore`
|
|
46
55
|
usually leaves `.git` out:
|
|
47
56
|
|
|
57
|
+
```bash
|
|
58
|
+
bundle exec deployangel install docker
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
adds this to the end of the Dockerfile's last stage, before its `CMD` (so the
|
|
62
|
+
layers above it stay cached):
|
|
63
|
+
|
|
48
64
|
```dockerfile
|
|
49
65
|
ARG GIT_SHA
|
|
50
66
|
ENV DEPLOYANGEL_REVISION=$GIT_SHA
|
|
51
67
|
```
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
Then build with `docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .`
|
|
70
|
+
(`fly deploy --build-arg GIT_SHA=$(git rev-parse HEAD)` on Fly.io, or
|
|
71
|
+
`build-args: GIT_SHA=${{ github.sha }}` with GitHub's docker/build-push-action).
|
|
72
|
+
It changes nothing if the Dockerfile already sets `DEPLOYANGEL_REVISION`, and
|
|
73
|
+
never edits CI workflows. A build without the argument leaves the variable
|
|
74
|
+
empty, which counts as unset. On DigitalOcean App Platform, set
|
|
75
|
+
`DEPLOYANGEL_REVISION: ${_self.COMMIT_HASH}` in the app spec. On Elastic
|
|
76
|
+
Beanstalk, see [Deploying to Elastic Beanstalk](#deploying-to-elastic-beanstalk).
|
|
77
|
+
If the agent finds none of these, its telemetry can't be tied to a deploy, and
|
|
78
|
+
the dashboard says so.
|
|
59
79
|
|
|
60
80
|
## What it sends
|
|
61
81
|
|
|
@@ -122,7 +142,7 @@ Environment variables are enough for most apps. To override in code:
|
|
|
122
142
|
```ruby
|
|
123
143
|
# config/initializers/deployangel.rb
|
|
124
144
|
DeployAngel.configure do |config|
|
|
125
|
-
config.environments = %w[production
|
|
145
|
+
config.environments = %w[production] # default: every environment but development and test
|
|
126
146
|
end
|
|
127
147
|
```
|
|
128
148
|
|
|
@@ -298,6 +318,9 @@ It writes `.kamal/hooks/post-deploy`, which runs `deployangel release` after eac
|
|
|
298
318
|
to add). Set `DEPLOYANGEL_API_TOKEN` (a CI deploys token) wherever you run
|
|
299
319
|
`kamal deploy`. The hook never fails a deploy.
|
|
300
320
|
|
|
321
|
+
Kamal apps don't need `deployangel install docker`: `KAMAL_VERSION` already
|
|
322
|
+
identifies the release.
|
|
323
|
+
|
|
301
324
|
### Deploying with Capistrano
|
|
302
325
|
|
|
303
326
|
Capistrano writes a `REVISION` file into each release, so the agent already
|
data/lib/deployangel/agent.rb
CHANGED
|
@@ -16,7 +16,7 @@ module DeployAngel
|
|
|
16
16
|
# later still lands in time.
|
|
17
17
|
FLUSH_JITTER = 1.0..50.0
|
|
18
18
|
|
|
19
|
-
attr_reader :config, :
|
|
19
|
+
attr_reader :config, :instance
|
|
20
20
|
attr_writer :metadata
|
|
21
21
|
|
|
22
22
|
def initialize(config:, environment:, root: nil, framework: nil, framework_version: nil,
|
|
@@ -24,6 +24,11 @@ module DeployAngel
|
|
|
24
24
|
@config = config
|
|
25
25
|
@active = config.active?(environment)
|
|
26
26
|
@release = Release.resolve(config: config, env: env, root: root)
|
|
27
|
+
# Nothing named the release, so the code fingerprint may. It hashes the
|
|
28
|
+
# app's files, so the reporter computes it before its first send (see
|
|
29
|
+
# #release), never at boot or in a request.
|
|
30
|
+
@fingerprint_pending = @release.unknown?
|
|
31
|
+
@release_mutex = Mutex.new
|
|
27
32
|
@runtime = Protocol.runtime(framework: framework, framework_version: framework_version)
|
|
28
33
|
@transport = transport || Transport.new(config)
|
|
29
34
|
@env = env
|
|
@@ -33,8 +38,6 @@ module DeployAngel
|
|
|
33
38
|
@thread_mutex = Mutex.new
|
|
34
39
|
@warned = {}
|
|
35
40
|
reset_process_state
|
|
36
|
-
warn_once(:unknown_release, "DeployAngel could not determine the release; set DEPLOYANGEL_REVISION " \
|
|
37
|
-
"or enable Heroku dyno metadata. Telemetry will not be attributed to deployments.") if @active && @release.unknown?
|
|
38
41
|
start_reporter if @active && eager
|
|
39
42
|
end
|
|
40
43
|
|
|
@@ -42,6 +45,25 @@ module DeployAngel
|
|
|
42
45
|
@active
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# The release this process is running. The first call may build the file
|
|
49
|
+
# digests for the code fingerprint, so only the reporter thread and
|
|
50
|
+
# shutdown call it; requests and jobs never read the release.
|
|
51
|
+
def release
|
|
52
|
+
return @release unless @fingerprint_pending
|
|
53
|
+
|
|
54
|
+
@release_mutex.synchronize do
|
|
55
|
+
if @fingerprint_pending
|
|
56
|
+
@release = Release.code_fingerprint(@metadata&.file_manifest) || @release
|
|
57
|
+
@fingerprint_pending = false
|
|
58
|
+
warn_about_release
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
@release
|
|
62
|
+
rescue StandardError
|
|
63
|
+
@fingerprint_pending = false
|
|
64
|
+
@release
|
|
65
|
+
end
|
|
66
|
+
|
|
45
67
|
def record_request(route_key:, status:, duration_ms:, unhandled: false, in_totals: true)
|
|
46
68
|
return unless @active
|
|
47
69
|
|
|
@@ -113,7 +135,7 @@ module DeployAngel
|
|
|
113
135
|
return 0 unless @active
|
|
114
136
|
|
|
115
137
|
@aggregator.drain(include_current: include_current, max_periods: config.max_queued_payloads).each do |period|
|
|
116
|
-
@buffer.push(Transport.encode(Protocol.telemetry(period, instance: @instance, release:
|
|
138
|
+
@buffer.push(Transport.encode(Protocol.telemetry(period, instance: @instance, release: release,
|
|
117
139
|
runtime: @runtime, capabilities: capabilities)))
|
|
118
140
|
end
|
|
119
141
|
send_buffered
|
|
@@ -162,7 +184,7 @@ module DeployAngel
|
|
|
162
184
|
return if @metadata_sent || @metadata.nil? || paused?
|
|
163
185
|
|
|
164
186
|
base = { "protocol_version" => Protocol::VERSION, "instance" => @instance.to_protocol,
|
|
165
|
-
"release" =>
|
|
187
|
+
"release" => release.to_protocol, "runtime" => @runtime }.merge(@metadata.to_protocol)
|
|
166
188
|
result = @transport.post(METADATA_PATH, base)
|
|
167
189
|
return unless result.ok?
|
|
168
190
|
|
|
@@ -187,6 +209,10 @@ module DeployAngel
|
|
|
187
209
|
end
|
|
188
210
|
|
|
189
211
|
def run_reporter
|
|
212
|
+
# Off the request path, so a process that exits within a minute or two
|
|
213
|
+
# doesn't build the fingerprint at shutdown instead. Only once the
|
|
214
|
+
# framework has attached the metadata it's built from.
|
|
215
|
+
release if @metadata
|
|
190
216
|
until @stopping
|
|
191
217
|
sleep(seconds_until_next_flush)
|
|
192
218
|
break if @stopping
|
|
@@ -242,6 +268,18 @@ module DeployAngel
|
|
|
242
268
|
@paused_until && @clock.call < @paused_until
|
|
243
269
|
end
|
|
244
270
|
|
|
271
|
+
def warn_about_release
|
|
272
|
+
return unless @active
|
|
273
|
+
|
|
274
|
+
if @release.unknown?
|
|
275
|
+
warn_once(:unknown_release, "DeployAngel could not determine the release; set DEPLOYANGEL_REVISION " \
|
|
276
|
+
"or enable Heroku dyno metadata. Telemetry will not be attributed to deployments.")
|
|
277
|
+
elsif @release.source == "code_fingerprint"
|
|
278
|
+
warn_once(:code_fingerprint, "DeployAngel identifies releases by a fingerprint of the app's code. " \
|
|
279
|
+
"Set DEPLOYANGEL_REVISION to the deployed commit to see each release's commits and pull requests.")
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
245
283
|
def warn_once(key, message)
|
|
246
284
|
return if @warned[key]
|
|
247
285
|
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -33,7 +33,8 @@ module DeployAngel
|
|
|
33
33
|
exception Details for a fingerprint deployangel exception FINGERPRINT
|
|
34
34
|
check Report a smoke test result --name=NAME --status=pass|fail [--covers=a,b]
|
|
35
35
|
[--commit=SHA | --deployment=ID] [--details-url=URL]
|
|
36
|
-
install Add a deploy hook
|
|
36
|
+
install Add a Kamal deploy hook, or deployangel install kamal
|
|
37
|
+
bake the commit into an image deployangel install docker
|
|
37
38
|
mcp Run the MCP server over stdio (for coding agents)
|
|
38
39
|
|
|
39
40
|
With no target, verify, plan, and check use the current git HEAD commit.
|
|
@@ -57,6 +58,25 @@ module DeployAngel
|
|
|
57
58
|
bundle exec deployangel release || true
|
|
58
59
|
SH
|
|
59
60
|
|
|
61
|
+
# Goes at the end of the Dockerfile's last stage: a value that changes
|
|
62
|
+
# with every commit invalidates the cache of every layer after it.
|
|
63
|
+
DOCKER_BUILD_HELP = <<~HELP.chomp
|
|
64
|
+
Pass the commit when you build the image:
|
|
65
|
+
|
|
66
|
+
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .
|
|
67
|
+
fly deploy --build-arg GIT_SHA=$(git rev-parse HEAD)
|
|
68
|
+
GitHub Actions (docker/build-push-action):
|
|
69
|
+
build-args: GIT_SHA=${{ github.sha }}
|
|
70
|
+
|
|
71
|
+
Kamal apps don't need this: DeployAngel reads Kamal's KAMAL_VERSION.
|
|
72
|
+
HELP
|
|
73
|
+
|
|
74
|
+
DOCKERFILE_LINES = <<~DOCKERFILE
|
|
75
|
+
# The commit this image runs, for DeployAngel. Build with --build-arg GIT_SHA=$(git rev-parse HEAD).
|
|
76
|
+
ARG GIT_SHA
|
|
77
|
+
ENV DEPLOYANGEL_REVISION=$GIT_SHA
|
|
78
|
+
DOCKERFILE
|
|
79
|
+
|
|
60
80
|
def initialize(argv, env: ENV, stdin: $stdin, stdout: $stdout, stderr: $stderr, client: nil,
|
|
61
81
|
sleeper: ->(seconds) { sleep(seconds) }, clock: -> { Time.now }, git_head: nil, root: Dir.pwd)
|
|
62
82
|
@root = root
|
|
@@ -206,12 +226,17 @@ module DeployAngel
|
|
|
206
226
|
0
|
|
207
227
|
end
|
|
208
228
|
|
|
209
|
-
# Writes .kamal/hooks/post-deploy, or says what to add to a hook that
|
|
210
|
-
# already exists, rather than overwriting it.
|
|
211
229
|
def install
|
|
212
|
-
target = @argv.shift
|
|
213
|
-
|
|
230
|
+
case (target = @argv.shift)
|
|
231
|
+
when "kamal" then install_kamal
|
|
232
|
+
when "docker" then install_docker
|
|
233
|
+
else usage_error("install needs a target: deployangel install kamal, or deployangel install docker")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
214
236
|
|
|
237
|
+
# Writes .kamal/hooks/post-deploy, or says what to add to a hook that
|
|
238
|
+
# already exists, rather than overwriting it.
|
|
239
|
+
def install_kamal
|
|
215
240
|
relative = File.join(".kamal", "hooks", "post-deploy")
|
|
216
241
|
path = File.join(@root, relative)
|
|
217
242
|
if File.exist?(path)
|
|
@@ -231,6 +256,55 @@ module DeployAngel
|
|
|
231
256
|
0
|
|
232
257
|
end
|
|
233
258
|
|
|
259
|
+
# Adds DEPLOYANGEL_REVISION, from a GIT_SHA build arg, to the Dockerfile.
|
|
260
|
+
# It never edits CI workflows; it says what to pass instead.
|
|
261
|
+
def install_docker
|
|
262
|
+
path = File.join(@root, "Dockerfile")
|
|
263
|
+
unless File.file?(path)
|
|
264
|
+
@stderr.puts("deployangel: no Dockerfile in #{@root}; run this where your Dockerfile is")
|
|
265
|
+
return USAGE_ERROR
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
content = File.read(path)
|
|
269
|
+
if content.include?("DEPLOYANGEL_REVISION")
|
|
270
|
+
@stdout.puts("Dockerfile already sets DEPLOYANGEL_REVISION.")
|
|
271
|
+
return 0
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
File.write(path, with_revision(content))
|
|
275
|
+
@stdout.puts("Added DEPLOYANGEL_REVISION to the Dockerfile's last stage. #{DOCKER_BUILD_HELP}")
|
|
276
|
+
0
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Before the CMD and ENTRYPOINT lines that end the last stage, with any
|
|
280
|
+
# comment just above them, or at the end if the stage doesn't end with
|
|
281
|
+
# one. Continuation lines aren't instructions.
|
|
282
|
+
def with_revision(content)
|
|
283
|
+
lines = content.lines
|
|
284
|
+
instructions = []
|
|
285
|
+
continuing = false
|
|
286
|
+
lines.each_with_index do |line, index|
|
|
287
|
+
text = line.strip
|
|
288
|
+
next if text.empty? || text.start_with?("#")
|
|
289
|
+
|
|
290
|
+
instructions << [ index, text[/\A\w+/].to_s.upcase ] unless continuing
|
|
291
|
+
continuing = text.end_with?("\\")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
stage = instructions.drop(instructions.rindex { |_, word| word == "FROM" } || 0)
|
|
295
|
+
trailing = stage.reverse.take_while { |_, word| %w[CMD ENTRYPOINT].include?(word) }
|
|
296
|
+
if trailing.empty?
|
|
297
|
+
lines[-1] = "#{lines[-1].chomp}\n" if lines.any?
|
|
298
|
+
lines << "\n" unless lines.empty? || lines[-1].strip.empty?
|
|
299
|
+
lines << DOCKERFILE_LINES
|
|
300
|
+
else
|
|
301
|
+
at = trailing.last.first
|
|
302
|
+
at -= 1 while at.positive? && lines[at - 1].strip.start_with?("#")
|
|
303
|
+
lines.insert(at, DOCKERFILE_LINES)
|
|
304
|
+
end
|
|
305
|
+
lines.join
|
|
306
|
+
end
|
|
307
|
+
|
|
234
308
|
def mcp
|
|
235
309
|
require_relative "mcp"
|
|
236
310
|
MCP::Server.new(client: client, input: @stdin, output: @stdout, error_output: @stderr,
|
|
@@ -17,7 +17,7 @@ module DeployAngel
|
|
|
17
17
|
@token = env["DEPLOYANGEL_TOKEN"]
|
|
18
18
|
@endpoint = env["DEPLOYANGEL_URL"] || DEFAULT_ENDPOINT
|
|
19
19
|
@enabled = parse_boolean(env["DEPLOYANGEL_ENABLED"])
|
|
20
|
-
@environments =
|
|
20
|
+
@environments = nil
|
|
21
21
|
@release_version = env["DEPLOYANGEL_RELEASE_VERSION"]
|
|
22
22
|
@revision = env["DEPLOYANGEL_REVISION"]
|
|
23
23
|
@flush_interval = 60
|
|
@@ -38,13 +38,19 @@ module DeployAngel
|
|
|
38
38
|
@sidekiq_cron_schedule_file = nil
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Reports only with a token
|
|
42
|
-
#
|
|
41
|
+
# Reports only with a token, and by default in every environment but
|
|
42
|
+
# these, so staging and preview apps work as they are while a token
|
|
43
|
+
# copied into a developer's .env stays quiet.
|
|
44
|
+
NON_REPORTING_ENVIRONMENTS = %w[development test].freeze
|
|
45
|
+
|
|
46
|
+
# config.environments, when set, lists the only environments to report
|
|
47
|
+
# from; DEPLOYANGEL_ENABLED forces it on or off.
|
|
43
48
|
def active?(environment)
|
|
44
49
|
return false if token.to_s.empty? || endpoint.to_s.empty?
|
|
45
50
|
return enabled unless enabled.nil?
|
|
51
|
+
return environments.map(&:to_s).include?(environment.to_s) if environments
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
!NON_REPORTING_ENVIRONMENTS.include?(environment.to_s)
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
# Whether requests to a route are left out. Rails answers HEAD with the
|
|
@@ -13,9 +13,15 @@ module DeployAngel
|
|
|
13
13
|
# Tags that move from build to build, so they can't identify a release.
|
|
14
14
|
MOVING_TAGS = %w[latest main master production prod staging stable release].freeze
|
|
15
15
|
|
|
16
|
+
# Look for .git this many directories above the app's root, for an app
|
|
17
|
+
# that lives in a subdirectory of its repository.
|
|
18
|
+
GIT_PARENT_LEVELS = 3
|
|
19
|
+
|
|
16
20
|
# Order: explicit configuration, Heroku dyno metadata, the hosting
|
|
17
21
|
# platform's own variables (Kamal, Render, Fly.io, Railway, Coolify, and
|
|
18
|
-
# Dokku's GIT_REV), a REVISION file, then ECS container
|
|
22
|
+
# Dokku's GIT_REV), a REVISION file, a git checkout, then ECS container
|
|
23
|
+
# metadata. When none of them names the release, the agent falls back to
|
|
24
|
+
# a fingerprint of the app's code (code_fingerprint), computed later.
|
|
19
25
|
def self.resolve(config:, env: ENV, root: nil, http: method(:fetch_metadata))
|
|
20
26
|
if present?(config.release_version) || present?(config.revision)
|
|
21
27
|
build(config.release_version, config.revision, "config")
|
|
@@ -36,6 +42,8 @@ module DeployAngel
|
|
|
36
42
|
build(nil, env["GIT_REV"], "git_rev")
|
|
37
43
|
elsif root && File.file?(revision_path = File.join(root, "REVISION"))
|
|
38
44
|
build(nil, File.read(revision_path, 100), "revision_file")
|
|
45
|
+
elsif root && (commit = git_head(root))
|
|
46
|
+
build(nil, commit, "git_head")
|
|
39
47
|
elsif present?(env["ECS_CONTAINER_METADATA_URI_V4"]) && (release = ecs(http.call(env["ECS_CONTAINER_METADATA_URI_V4"])))
|
|
40
48
|
release
|
|
41
49
|
else
|
|
@@ -73,6 +81,77 @@ module DeployAngel
|
|
|
73
81
|
tag unless tag.empty?
|
|
74
82
|
end
|
|
75
83
|
|
|
84
|
+
# A server deployed by `git pull` (or Fabric, or Ansible) runs from a
|
|
85
|
+
# checkout, where HEAD names the commit. Read from the repository's
|
|
86
|
+
# files, never by running git. Nil if anything is missing or unreadable.
|
|
87
|
+
def self.git_head(root)
|
|
88
|
+
git_dir = find_git_dir(File.expand_path(root)) or return
|
|
89
|
+
common_dir = git_dir
|
|
90
|
+
commondir = File.join(git_dir, "commondir")
|
|
91
|
+
common_dir = File.expand_path(File.read(commondir, 1024).strip, git_dir) if File.file?(commondir)
|
|
92
|
+
|
|
93
|
+
head = File.read(File.join(git_dir, "HEAD"), 200).to_s.strip
|
|
94
|
+
ref = head.delete_prefix("ref:").strip if head.start_with?("ref:")
|
|
95
|
+
commit = ref ? read_ref(ref, git_dir, common_dir) : head
|
|
96
|
+
commit = commit.to_s.strip.downcase
|
|
97
|
+
commit if commit.match?(COMMIT_FORMAT)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# .git is a directory, or for a worktree or submodule, a file naming the
|
|
103
|
+
# real one ("gitdir: ../.git/worktrees/shop").
|
|
104
|
+
def self.find_git_dir(dir)
|
|
105
|
+
(GIT_PARENT_LEVELS + 1).times do
|
|
106
|
+
git = File.join(dir, ".git")
|
|
107
|
+
return File.directory?(git) ? git : linked_git_dir(git) if File.exist?(git)
|
|
108
|
+
|
|
109
|
+
parent = File.dirname(dir)
|
|
110
|
+
return if parent == dir
|
|
111
|
+
|
|
112
|
+
dir = parent
|
|
113
|
+
end
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.linked_git_dir(file)
|
|
118
|
+
path = File.read(file, 1024).to_s[/\Agitdir:\s*(.+)$/, 1]&.strip
|
|
119
|
+
File.expand_path(path, File.dirname(file)) if present?(path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A branch's commit is in its ref file, in the worktree's own git dir or
|
|
123
|
+
# the common one, or else in packed-refs. Refs come from a file on disk,
|
|
124
|
+
# so anything outside refs/ is ignored rather than read.
|
|
125
|
+
def self.read_ref(ref, git_dir, common_dir)
|
|
126
|
+
return unless ref.start_with?("refs/") && !ref.include?("..")
|
|
127
|
+
|
|
128
|
+
[ git_dir, common_dir ].uniq.each do |dir|
|
|
129
|
+
path = File.join(dir, ref)
|
|
130
|
+
return File.read(path, 100) if File.file?(path)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
packed = File.join(common_dir, "packed-refs")
|
|
134
|
+
return unless File.file?(packed)
|
|
135
|
+
|
|
136
|
+
File.foreach(packed) do |line|
|
|
137
|
+
next if line.start_with?("#", "^")
|
|
138
|
+
|
|
139
|
+
commit, name = line.split(" ", 2)
|
|
140
|
+
return commit if name.to_s.strip == ref
|
|
141
|
+
end
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The release of an app that names it no other way: the hash of the file
|
|
146
|
+
# digests the agent already sends (Metadata#file_manifest), so the same
|
|
147
|
+
# code gives the same release. None when digests are off or truncated.
|
|
148
|
+
def self.code_fingerprint(manifest)
|
|
149
|
+
return unless manifest.is_a?(Hash) && !manifest["truncated"] && manifest["count"].to_i.positive?
|
|
150
|
+
|
|
151
|
+
hash = manifest["hash"].to_s
|
|
152
|
+
build("code:#{hash[0, 12]}", nil, "code_fingerprint") if hash.match?(/\A\h{12}/)
|
|
153
|
+
end
|
|
154
|
+
|
|
76
155
|
# ECS (Fargate, and EC2 with a recent agent) serves each container's
|
|
77
156
|
# metadata at ECS_CONTAINER_METADATA_URI_V4. The image tag identifies the
|
|
78
157
|
# release, and is the commit when it looks like one. A moving tag such
|
data/lib/deployangel/metadata.rb
CHANGED
|
@@ -167,6 +167,8 @@ module DeployAngel
|
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# Built once per process, by the reporter: the metadata send and the
|
|
171
|
+
# code fingerprint (Release.code_fingerprint) both read it.
|
|
170
172
|
def file_manifest
|
|
171
173
|
@file_manifest ||= begin
|
|
172
174
|
if @config.file_digests
|
|
@@ -180,9 +182,9 @@ module DeployAngel
|
|
|
180
182
|
else
|
|
181
183
|
{ "hash" => nil, "count" => 0, "truncated" => false, "files" => {} }
|
|
182
184
|
end
|
|
185
|
+
rescue StandardError
|
|
186
|
+
{ "hash" => nil, "count" => 0, "truncated" => false, "files" => {} }
|
|
183
187
|
end
|
|
184
|
-
rescue StandardError
|
|
185
|
-
{ "hash" => nil, "count" => 0, "truncated" => false, "files" => {} }
|
|
186
188
|
end
|
|
187
189
|
end
|
|
188
190
|
end
|
data/lib/deployangel/version.rb
CHANGED