onair-cli 0.2.0 → 0.3.0
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 +7 -0
- data/README.md +19 -1
- data/lib/onair/cli.rb +5 -4
- data/lib/onair/git.rb +1 -1
- data/lib/onair/orchestrator.rb +3 -2
- data/lib/onair/platform/base.rb +4 -1
- data/lib/onair/platform/heroku.rb +64 -5
- data/lib/onair/remote_head.rb +1 -1
- data/lib/onair/renderer/json.rb +23 -7
- data/lib/onair/renderer/tty.rb +36 -6
- data/lib/onair/report.rb +14 -5
- data/lib/onair/version.rb +1 -1
- data/lib/onair.rb +18 -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: 1eb5f2eb79c1eb2da95ef69cb65f8bef60b72dc3d98563a6235573b4ae139fd6
|
|
4
|
+
data.tar.gz: f5a38c5ef53340a88e41c9c007e68c34d1035d0427b953ac7b4193421201dde7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 133e4480b8e8c22835294d0c2b18e79f9ee6ba065ee080c7c5f0e0c61a1a8bf92aecc6ee2e6092bce8ba095ffe7937ce8b6cb6526ca20938ae3d3d54dc39062e
|
|
7
|
+
data.tar.gz: b4e08f469025a62273c6c848b4d790e8b9a1520a6ce6ab24f0b845bcd7453cfa2040b1d40b3d0a29fe73c13e9c3d4242779022b0b2c43ba11c968f0e5ae74be5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
- Heroku: a rollout line under Deployed counts the dynos of each process type
|
|
8
|
+
running the deployed release (`web 1/3, 2 starting`) and says when every
|
|
9
|
+
dyno is up on it. With preboot on, the handoff from the previous web dynos
|
|
10
|
+
shows as an estimate. `--json` gains a `rollout` object.
|
|
11
|
+
|
|
5
12
|
## [0.2.0] - 2026-10-07
|
|
6
13
|
|
|
7
14
|
- Heroku: Deployed is the release Heroku is serving, not the newest one. A
|
data/README.md
CHANGED
|
@@ -72,6 +72,11 @@ the API is a faster transport, never a cache.
|
|
|
72
72
|
author, and a delta against `origin/main`: `★ current` or
|
|
73
73
|
`↓ N commits behind`. If the relationship is unknown (diverged history,
|
|
74
74
|
commits unavailable), no marker is shown - silence over speculation.
|
|
75
|
+
- **Rollout** - under the Deployed row, how many dynos of each process type
|
|
76
|
+
run that release: `⟳ rolling out v1234: web 1/3, 2 starting` while dynos
|
|
77
|
+
restart, `✓ rolled out` once every dyno is up on it. With preboot on, the
|
|
78
|
+
previous web dynos keep serving for about three minutes after the new ones
|
|
79
|
+
are up; that window shows as an estimate.
|
|
75
80
|
- **Pending** - an in-flight build that's about to replace the deploy.
|
|
76
81
|
- **Releasing** - a newer release whose release phase (e.g. migrations) is
|
|
77
82
|
still running; production still serves the Deployed row until it finishes.
|
|
@@ -130,6 +135,13 @@ additively:
|
|
|
130
135
|
},
|
|
131
136
|
"pending": null,
|
|
132
137
|
"release": null,
|
|
138
|
+
"rollout": {
|
|
139
|
+
"version": 1234, "complete": true,
|
|
140
|
+
"processes": [
|
|
141
|
+
{ "type": "web", "up": 3, "total": 3, "waiting": {}, "previous": 0 }
|
|
142
|
+
],
|
|
143
|
+
"overlap_until": null
|
|
144
|
+
},
|
|
133
145
|
"delta": { "status": "current", "behind_by": 0 },
|
|
134
146
|
"pinned": null,
|
|
135
147
|
"yours": null
|
|
@@ -143,6 +155,12 @@ additively:
|
|
|
143
155
|
"subject", "author" }` when a release newer than the running one is not
|
|
144
156
|
live; `status` is `"pending"` (release phase running) or `"failed"`. While
|
|
145
157
|
it carries a different sha, `delta.status` is never `"current"`.
|
|
158
|
+
- `rollout`: dynos per process type against the running release. `up`
|
|
159
|
+
counts dynos serving it, `waiting` counts the rest of its dynos by state
|
|
160
|
+
(`starting`, `crashed`, ...), `previous` counts dynos on an older release.
|
|
161
|
+
`overlap_until` is an estimated end of the preboot handoff while it lasts,
|
|
162
|
+
otherwise `null`. `complete` is true once every dyno serves the running
|
|
163
|
+
release and no handoff remains. `null` when the dynos can't be read.
|
|
146
164
|
- `pinned`: `{ "version", "description", "latest_built_sha" }` when a newer
|
|
147
165
|
build succeeded but is not running.
|
|
148
166
|
- `yours`: `{ "sha", "had_own_build", "subject", "author" }` when your commit
|
|
@@ -155,7 +173,7 @@ additively:
|
|
|
155
173
|
|
|
156
174
|
```sh
|
|
157
175
|
bundle install
|
|
158
|
-
bundle exec rake # specs +
|
|
176
|
+
bundle exec rake # specs + standard
|
|
159
177
|
```
|
|
160
178
|
|
|
161
179
|
The behavioral source of truth is `reference/prod-release.sh`, the
|
data/lib/onair/cli.rb
CHANGED
|
@@ -64,16 +64,17 @@ module Onair
|
|
|
64
64
|
git = Git.new(fetch_allowed: config.fetch)
|
|
65
65
|
adapter = Platform.build(config)
|
|
66
66
|
repo = config.repo || git.origin_repo
|
|
67
|
-
|
|
67
|
+
now = Time.now
|
|
68
|
+
report = Orchestrator.new(config: config, adapter: adapter, git: git, repo: repo, now: now).run
|
|
68
69
|
|
|
69
70
|
if flags[:json]
|
|
70
71
|
puts Renderer::Json.new(report: report, app: config.app, platform: config.platform,
|
|
71
|
-
|
|
72
|
+
branch: config.branch, repo: repo, task: config.task).render
|
|
72
73
|
else
|
|
73
74
|
tty = $stdout.tty?
|
|
74
75
|
print Renderer::Tty.new(report: report, app: config.app, platform_label: adapter.display_name,
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
branch: config.branch, repo: repo, task: config.task,
|
|
77
|
+
color: tty && !no_color?, hyperlinks: tty, now: now).render
|
|
77
78
|
end
|
|
78
79
|
end
|
|
79
80
|
|
data/lib/onair/git.rb
CHANGED
data/lib/onair/orchestrator.rb
CHANGED
|
@@ -4,11 +4,12 @@ module Onair
|
|
|
4
4
|
# Runs the platform adapter and git concurrently, lazily fetches missing
|
|
5
5
|
# commits at most once, and assembles the Report.
|
|
6
6
|
class Orchestrator
|
|
7
|
-
def initialize(config:, adapter:, git:, repo: nil)
|
|
7
|
+
def initialize(config:, adapter:, git:, repo: nil, now: Time.now)
|
|
8
8
|
@config = config
|
|
9
9
|
@adapter = adapter
|
|
10
10
|
@git = git
|
|
11
11
|
@repo = repo
|
|
12
|
+
@now = now
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def run
|
|
@@ -20,7 +21,7 @@ module Onair
|
|
|
20
21
|
needed = [snapshot.deployed&.sha, snapshot.pending&.sha, snapshot.release&.sha, remote_head].compact
|
|
21
22
|
@git.fetch_once! if needed.any? { |sha| !@git.has_commit?(sha) }
|
|
22
23
|
|
|
23
|
-
Report.build(snapshot: snapshot, remote_head: remote_head, git: @git)
|
|
24
|
+
Report.build(snapshot: snapshot, remote_head: remote_head, git: @git, now: @now)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
private
|
data/lib/onair/platform/base.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Onair
|
|
|
13
13
|
klass = @registry[config.platform]
|
|
14
14
|
if klass.nil?
|
|
15
15
|
raise Error,
|
|
16
|
-
|
|
16
|
+
"unknown platform #{config.platform.inspect} (available: #{@registry.keys.join(", ")})"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
klass.new(config)
|
|
@@ -29,6 +29,9 @@ module Onair
|
|
|
29
29
|
# running (release phase pending or failed), or nil
|
|
30
30
|
# latest_built_sha: newest successfully built sha, or nil
|
|
31
31
|
# succeeded_shas: recent succeeded build shas, newest first
|
|
32
|
+
# rollout: per process type, how many dynos run the running
|
|
33
|
+
# release, plus an optional estimate of when the
|
|
34
|
+
# previous release stops serving; nil when unknown
|
|
32
35
|
#
|
|
33
36
|
# Adapters own their internal concurrency and auth. Everything above
|
|
34
37
|
# (delta, pinned, mine, rendering) is platform-agnostic and must not
|
|
@@ -10,15 +10,24 @@ module Onair
|
|
|
10
10
|
HOST = "api.heroku.com"
|
|
11
11
|
RELEASE_WINDOW = 10
|
|
12
12
|
IN_FLIGHT_STATUSES = %w[pending failed].freeze
|
|
13
|
+
ONE_OFF_TYPES = %w[run scheduler release].freeze
|
|
14
|
+
# An eco dyno asleep on the running release boots that release on wake.
|
|
15
|
+
SERVING_STATES = %w[up idle].freeze
|
|
16
|
+
# With preboot, Heroku keeps routing to the previous web dynos for about
|
|
17
|
+
# three minutes after the new ones come up.
|
|
18
|
+
PREBOOT_HANDOFF = 180
|
|
13
19
|
|
|
14
20
|
def snapshot
|
|
15
21
|
token = resolve_token
|
|
16
22
|
releases_thread = quiet_thread { releases(token) }
|
|
17
23
|
builds_thread = quiet_thread { builds(token) }
|
|
24
|
+
dynos_thread = quiet_thread { dynos(token) }
|
|
18
25
|
deployed, release = releases_thread.value
|
|
19
26
|
pending, succeeded_shas = builds_thread.value
|
|
27
|
+
dyno_rows, preboot = dynos_thread.value
|
|
20
28
|
Snapshot.new(deployed: deployed, pending: pending, release: release,
|
|
21
|
-
|
|
29
|
+
latest_built_sha: succeeded_shas.first, succeeded_shas: succeeded_shas,
|
|
30
|
+
rollout: rollout(dyno_rows, preboot, deployed.version))
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
private
|
|
@@ -62,7 +71,7 @@ module Onair
|
|
|
62
71
|
|
|
63
72
|
def deployed(row, sha)
|
|
64
73
|
Deployed.new(sha: sha, version: row["version"], description: row["description"],
|
|
65
|
-
|
|
74
|
+
deployed_at: parse_time(row["created_at"]))
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
# Only the newest release counts: an older failed one was superseded by
|
|
@@ -71,11 +80,11 @@ module Onair
|
|
|
71
80
|
return nil if newest.equal?(running) || !IN_FLIGHT_STATUSES.include?(newest["status"])
|
|
72
81
|
|
|
73
82
|
slug_id = newest.dig("slug", "id")
|
|
74
|
-
sha = slug_id == running.dig("slug", "id") ? running_sha : slug_commit(http, token, slug_id)
|
|
83
|
+
sha = (slug_id == running.dig("slug", "id")) ? running_sha : slug_commit(http, token, slug_id)
|
|
75
84
|
return nil if sha.nil?
|
|
76
85
|
|
|
77
86
|
Release.new(sha: sha, version: newest["version"], description: newest["description"],
|
|
78
|
-
|
|
87
|
+
status: newest["status"].to_sym, started_at: parse_time(newest["created_at"]))
|
|
79
88
|
end
|
|
80
89
|
|
|
81
90
|
def slug_commit(http, token, slug_id)
|
|
@@ -96,12 +105,62 @@ module Onair
|
|
|
96
105
|
pending_sha = pending_build&.dig("source_blob", "version")
|
|
97
106
|
pending = pending_sha && Pending.new(sha: pending_sha, started_at: parse_time(pending_build["created_at"]))
|
|
98
107
|
succeeded = builds.select { |build| build["status"] == "succeeded" }
|
|
99
|
-
|
|
108
|
+
.filter_map { |build| build.dig("source_blob", "version") }
|
|
100
109
|
[pending, succeeded]
|
|
101
110
|
rescue Error
|
|
102
111
|
[nil, []]
|
|
103
112
|
end
|
|
104
113
|
|
|
114
|
+
# A failed dynos call drops the rollout and nothing else; a failed
|
|
115
|
+
# preboot lookup only drops the handoff estimate.
|
|
116
|
+
def dynos(token)
|
|
117
|
+
with_http do |http|
|
|
118
|
+
rows = get(http, token, "/apps/#{app}/dynos")
|
|
119
|
+
[rows, preboot?(http, token)]
|
|
120
|
+
end
|
|
121
|
+
rescue Error
|
|
122
|
+
[nil, false]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def preboot?(http, token)
|
|
126
|
+
get(http, token, "/apps/#{app}/features/preboot")["enabled"] == true
|
|
127
|
+
rescue Error
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def rollout(rows, preboot, version)
|
|
132
|
+
return nil if rows.nil? || version.nil?
|
|
133
|
+
|
|
134
|
+
formation = rows.reject { |dyno| ONE_OFF_TYPES.include?(dyno["type"]) }
|
|
135
|
+
return nil if formation.empty?
|
|
136
|
+
|
|
137
|
+
processes = formation.group_by { |dyno| dyno["type"] }
|
|
138
|
+
.sort_by { |type, _| [(type == "web") ? 0 : 1, type] }
|
|
139
|
+
.map { |type, dynos| process_rollout(type, dynos, version) }
|
|
140
|
+
Rollout.new(version: version, processes: processes,
|
|
141
|
+
overlap_until: preboot ? preboot_handoff(formation, version) : nil)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def process_rollout(type, dynos, version)
|
|
145
|
+
current, previous = dynos.partition { |dyno| dyno.dig("release", "version") == version }
|
|
146
|
+
waiting = current.map { |dyno| dyno["state"] }.reject { |state| SERVING_STATES.include?(state) }.tally
|
|
147
|
+
ProcessRollout.new(type: type, total: dynos.size, up: current.size - waiting.values.sum,
|
|
148
|
+
waiting: waiting, previous: previous.size)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Old web dynos still listed are already counted as previous; the
|
|
152
|
+
# estimate covers only the handoff the dynos list does not show.
|
|
153
|
+
def preboot_handoff(formation, version)
|
|
154
|
+
web = formation.select { |dyno| dyno["type"] == "web" }
|
|
155
|
+
return nil if web.empty?
|
|
156
|
+
return nil unless web.all? do |dyno|
|
|
157
|
+
dyno.dig("release", "version") == version && SERVING_STATES.include?(dyno["state"])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
last_up = web.filter_map { |dyno| parse_time(dyno["updated_at"]) }.max
|
|
161
|
+
last_up && (last_up + PREBOOT_HANDOFF)
|
|
162
|
+
end
|
|
163
|
+
|
|
105
164
|
# One connection per thread — the dependent releases → slug pair must
|
|
106
165
|
# not pay a second TLS handshake.
|
|
107
166
|
def with_http(&)
|
data/lib/onair/remote_head.rb
CHANGED
data/lib/onair/renderer/json.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Onair
|
|
|
31
31
|
deployed: deployed_payload,
|
|
32
32
|
pending: pending_payload,
|
|
33
33
|
release: release_payload,
|
|
34
|
+
rollout: rollout_payload,
|
|
34
35
|
delta: delta_payload,
|
|
35
36
|
pinned: pinned_payload,
|
|
36
37
|
yours: yours_payload
|
|
@@ -59,7 +60,7 @@ module Onair
|
|
|
59
60
|
pending = snapshot.pending
|
|
60
61
|
return nil if pending.nil?
|
|
61
62
|
|
|
62
|
-
{
|
|
63
|
+
{sha: pending.sha, started_at: iso(pending.started_at)}.merge(commit_fields(pending.sha))
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def release_payload
|
|
@@ -75,11 +76,26 @@ module Onair
|
|
|
75
76
|
}.merge(commit_fields(release.sha))
|
|
76
77
|
end
|
|
77
78
|
|
|
79
|
+
def rollout_payload
|
|
80
|
+
rollout = @report.rollout
|
|
81
|
+
return nil if rollout.nil?
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
version: rollout.version,
|
|
85
|
+
complete: rollout.complete?,
|
|
86
|
+
processes: rollout.processes.map do |process|
|
|
87
|
+
{type: process.type, up: process.up, total: process.total,
|
|
88
|
+
waiting: process.waiting, previous: process.previous}
|
|
89
|
+
end,
|
|
90
|
+
overlap_until: iso(rollout.overlap_until)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
78
94
|
def delta_payload
|
|
79
95
|
case @report.delta
|
|
80
|
-
when :current then {
|
|
81
|
-
when Integer then {
|
|
82
|
-
else {
|
|
96
|
+
when :current then {status: "current", behind_by: 0}
|
|
97
|
+
when Integer then {status: "behind", behind_by: @report.delta}
|
|
98
|
+
else {status: "unknown", behind_by: nil}
|
|
83
99
|
end
|
|
84
100
|
end
|
|
85
101
|
|
|
@@ -98,19 +114,19 @@ module Onair
|
|
|
98
114
|
mine = @report.mine
|
|
99
115
|
return nil if mine.nil?
|
|
100
116
|
|
|
101
|
-
{
|
|
117
|
+
{sha: mine.sha, had_own_build: mine.had_own_build}.merge(commit_fields(mine.sha))
|
|
102
118
|
end
|
|
103
119
|
|
|
104
120
|
def commit_fields(sha)
|
|
105
121
|
info = sha && @report.commits[sha]
|
|
106
|
-
{
|
|
122
|
+
{subject: info&.subject, author: info&.author_name, task: task_fields(info&.subject)}
|
|
107
123
|
end
|
|
108
124
|
|
|
109
125
|
def task_fields(subject)
|
|
110
126
|
task_id = @task&.find(subject)
|
|
111
127
|
return nil if task_id.nil?
|
|
112
128
|
|
|
113
|
-
{
|
|
129
|
+
{id: task_id, url: @task.url_for(task_id)}
|
|
114
130
|
end
|
|
115
131
|
|
|
116
132
|
def iso(time)
|
data/lib/onair/renderer/tty.rb
CHANGED
|
@@ -61,10 +61,10 @@ module Onair
|
|
|
61
61
|
detail = "v#{release.version}"
|
|
62
62
|
detail = "#{detail}: #{release.description}" if release.sha == snapshot.deployed&.sha
|
|
63
63
|
label, color, note = if release.status == :failed
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
["Failed:", :red, "✗ release phase failed (#{detail})"]
|
|
65
|
+
else
|
|
66
|
+
["Releasing:", :yellow, "⟳ release phase (#{detail})"]
|
|
67
|
+
end
|
|
68
68
|
row_lines(label, color, release.sha, age(release.started_at), extra: paint(note, color)) + [""]
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -72,6 +72,7 @@ module Onair
|
|
|
72
72
|
deployed = snapshot.deployed
|
|
73
73
|
if deployed&.sha
|
|
74
74
|
lines = row_lines("Deployed:", :green, deployed.sha, age(deployed.deployed_at), extra: delta_text)
|
|
75
|
+
lines << rollout_line if @report.rollout
|
|
75
76
|
lines << pinned_line if @report.pinned
|
|
76
77
|
lines.concat(yours_lines)
|
|
77
78
|
lines
|
|
@@ -81,6 +82,35 @@ module Onair
|
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# The dynos list can look finished while preboot still routes some
|
|
86
|
+
# traffic to the previous release, so that window gets its own line.
|
|
87
|
+
def rollout_line
|
|
88
|
+
rollout = @report.rollout
|
|
89
|
+
text, color = if rollout.complete?
|
|
90
|
+
["✓ rolled out v#{rollout.version}: #{process_counts(rollout)}", :dim]
|
|
91
|
+
elsif rollout.processes.all?(&:complete?)
|
|
92
|
+
["⟳ preboot: previous web dynos may still serve for " \
|
|
93
|
+
"~#{remaining(rollout.overlap_until)} (estimate)", :yellow]
|
|
94
|
+
else
|
|
95
|
+
["⟳ rolling out v#{rollout.version}: #{process_counts(rollout)}", :yellow]
|
|
96
|
+
end
|
|
97
|
+
" #{paint(text, color)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def process_counts(rollout)
|
|
101
|
+
rollout.processes.map do |process|
|
|
102
|
+
parts = ["#{process.type} #{process.up}/#{process.total}"]
|
|
103
|
+
parts.concat(process.waiting.map { |state, count| "#{count} #{state}" })
|
|
104
|
+
parts << "#{process.previous} on older release" if process.previous.positive?
|
|
105
|
+
parts.join(", ")
|
|
106
|
+
end.join(" · ")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def remaining(time)
|
|
110
|
+
seconds = (time - @now).ceil.clamp(0..)
|
|
111
|
+
(seconds < 60) ? "#{seconds}s" : "#{(seconds / 60.0).ceil}m"
|
|
112
|
+
end
|
|
113
|
+
|
|
84
114
|
def yours_lines
|
|
85
115
|
mine = @report.mine
|
|
86
116
|
return [] if mine.nil?
|
|
@@ -98,7 +128,7 @@ module Onair
|
|
|
98
128
|
padded = label.ljust(LABEL_WIDTH)
|
|
99
129
|
first = " #{paint(padded, color_key)} #{paint(sha[0, 9], :bold)} #{paint("#{age_blurb}by #{author}", :dim)}"
|
|
100
130
|
first = "#{first} #{extra}" if extra
|
|
101
|
-
[first, " #{paint(
|
|
131
|
+
[first, " #{paint("→", :dim)} #{linkify(subject, info ? sha : nil)}"]
|
|
102
132
|
end
|
|
103
133
|
|
|
104
134
|
def delta_text
|
|
@@ -107,7 +137,7 @@ module Onair
|
|
|
107
137
|
paint("★ current", :green)
|
|
108
138
|
when Integer
|
|
109
139
|
count = @report.delta
|
|
110
|
-
paint("↓ #{count} #{count == 1 ?
|
|
140
|
+
paint("↓ #{count} #{(count == 1) ? "commit" : "commits"} behind origin/#{@branch}", :yellow)
|
|
111
141
|
end
|
|
112
142
|
end
|
|
113
143
|
|
data/lib/onair/report.rb
CHANGED
|
@@ -10,8 +10,10 @@ module Onair
|
|
|
10
10
|
# mine: the local user's commit just below someone else's deployed head.
|
|
11
11
|
# commits: sha => CommitInfo (or nil when absent locally) for every sha a
|
|
12
12
|
# renderer may need to describe.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# rollout: the snapshot's rollout, its handoff estimate dropped once `now`
|
|
14
|
+
# has passed it, or nil when unknown.
|
|
15
|
+
Report = Data.define(:snapshot, :remote_head, :delta, :pinned, :mine, :commits, :rollout) do
|
|
16
|
+
def self.build(snapshot:, remote_head:, git:, now:)
|
|
15
17
|
# Pinned is judged before the stale pending is dropped: during the
|
|
16
18
|
# stale window the newest *succeeded* build is still the previous
|
|
17
19
|
# deploy, which must not read as a rollback.
|
|
@@ -20,17 +22,24 @@ module Onair
|
|
|
20
22
|
deployed_sha = snapshot.deployed&.sha
|
|
21
23
|
mine = compute_mine(snapshot, git)
|
|
22
24
|
commits = [deployed_sha, snapshot.pending&.sha, snapshot.release&.sha, mine&.sha]
|
|
23
|
-
|
|
25
|
+
.compact.uniq.to_h { |sha| [sha, git.commit_info(sha)] }
|
|
24
26
|
new(
|
|
25
27
|
snapshot: snapshot,
|
|
26
28
|
remote_head: remote_head,
|
|
27
29
|
delta: compute_delta(snapshot, remote_head, git),
|
|
28
30
|
pinned: pinned,
|
|
29
31
|
mine: mine,
|
|
30
|
-
commits: commits
|
|
32
|
+
commits: commits,
|
|
33
|
+
rollout: live_rollout(snapshot.rollout, now)
|
|
31
34
|
)
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
def self.live_rollout(rollout, now)
|
|
38
|
+
return rollout if rollout.nil? || rollout.overlap_until.nil? || rollout.overlap_until > now
|
|
39
|
+
|
|
40
|
+
rollout.with(overlap_until: nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
# Right after a deploy finishes, the platform's builds list can still
|
|
35
44
|
# report the just-released build as pending while the releases endpoint
|
|
36
45
|
# already shows it running — the same commit would render as both
|
|
@@ -91,6 +100,6 @@ module Onair
|
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
private_class_method :drop_superseded_pending, :compute_delta, :pinned?, :release_of_other_commit?,
|
|
94
|
-
|
|
103
|
+
:compute_mine, :identity_match?, :live_rollout
|
|
95
104
|
end
|
|
96
105
|
end
|
data/lib/onair/version.rb
CHANGED
data/lib/onair.rb
CHANGED
|
@@ -18,10 +18,27 @@ module Onair
|
|
|
18
18
|
# release phase is still running (status :pending) or it failed (:failed).
|
|
19
19
|
Release = Data.define(:sha, :version, :description, :status, :started_at)
|
|
20
20
|
|
|
21
|
+
# Dynos of one process type against the running release. `up` counts dynos
|
|
22
|
+
# on the running version that serve (or idle until woken); `waiting` counts
|
|
23
|
+
# the rest of them by state; `previous` counts dynos on an older release.
|
|
24
|
+
ProcessRollout = Data.define(:type, :total, :up, :waiting, :previous) do
|
|
25
|
+
def complete?
|
|
26
|
+
up == total
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# How far the running release has reached its dynos. `overlap_until` is an
|
|
31
|
+
# estimate of when the previous release stops serving traffic, or nil.
|
|
32
|
+
Rollout = Data.define(:version, :processes, :overlap_until) do
|
|
33
|
+
def complete?
|
|
34
|
+
processes.all?(&:complete?) && overlap_until.nil?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
21
38
|
# What a platform adapter returns. `latest_built_sha` is the newest
|
|
22
39
|
# successfully built sha (rollback detection); `succeeded_shas` lists all
|
|
23
40
|
# recent succeeded build shas, newest first ("yours had its own deploy").
|
|
24
|
-
Snapshot = Data.define(:deployed, :pending, :release, :latest_built_sha, :succeeded_shas)
|
|
41
|
+
Snapshot = Data.define(:deployed, :pending, :release, :latest_built_sha, :succeeded_shas, :rollout)
|
|
25
42
|
|
|
26
43
|
Mine = Data.define(:sha, :had_own_build)
|
|
27
44
|
end
|