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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1faf09e85aa982fd64a664e4d1f566c4aaf836cc3f196cde37ef9477a42c498f
4
- data.tar.gz: 41404044b99b5624b947bc726cefb8e85910622f95ff31b2419bd12c612b56bb
3
+ metadata.gz: 1eb5f2eb79c1eb2da95ef69cb65f8bef60b72dc3d98563a6235573b4ae139fd6
4
+ data.tar.gz: f5a38c5ef53340a88e41c9c007e68c34d1035d0427b953ac7b4193421201dde7
5
5
  SHA512:
6
- metadata.gz: 2343601742f56a59b9141dd06d597f040ca1f55eae53295dd34dad9da10127372cb544b5bf9ff6bff98fdcaf1b2d938eab0e3ea0c9309f93ebd000da9a298566
7
- data.tar.gz: 4ae9c7a4348ded957fb88a922cc44d8ebebeba36f6c870303663fe922be631bed2b2ac5ea82acbfc4ac2111bffdeefc0e8ac05c60e8dbb8d9068c51a33fb1eee
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 + rubocop
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
- report = Orchestrator.new(config: config, adapter: adapter, git: git, repo: repo).run
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
- branch: config.branch, repo: repo, task: config.task).render
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
- branch: config.branch, repo: repo, task: config.task,
76
- color: tty && !no_color?, hyperlinks: tty, now: Time.now).render
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
@@ -91,7 +91,7 @@ module Onair
91
91
  end
92
92
 
93
93
  def opts
94
- @dir ? { chdir: @dir } : {}
94
+ @dir ? {chdir: @dir} : {}
95
95
  end
96
96
  end
97
97
  end
@@ -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
@@ -13,7 +13,7 @@ module Onair
13
13
  klass = @registry[config.platform]
14
14
  if klass.nil?
15
15
  raise Error,
16
- "unknown platform #{config.platform.inspect} (available: #{@registry.keys.join(', ')})"
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
- latest_built_sha: succeeded_shas.first, succeeded_shas: succeeded_shas)
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
- deployed_at: parse_time(row["created_at"]))
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
- status: newest["status"].to_sym, started_at: parse_time(newest["created_at"]))
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
- .filter_map { |build| build.dig("source_blob", "version") }
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(&)
@@ -38,7 +38,7 @@ module Onair
38
38
 
39
39
  sha = JSON.parse(response.body).dig("object", "sha")
40
40
  sha&.match?(/\A\h{40}\z/) ? sha : nil
41
- rescue StandardError
41
+ rescue
42
42
  nil
43
43
  end
44
44
  private_class_method :api_head
@@ -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
- { sha: pending.sha, started_at: iso(pending.started_at) }.merge(commit_fields(pending.sha))
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 { status: "current", behind_by: 0 }
81
- when Integer then { status: "behind", behind_by: @report.delta }
82
- else { status: "unknown", behind_by: nil }
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
- { sha: mine.sha, had_own_build: mine.had_own_build }.merge(commit_fields(mine.sha))
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
- { subject: info&.subject, author: info&.author_name, task: task_fields(info&.subject) }
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
- { id: task_id, url: @task.url_for(task_id) }
129
+ {id: task_id, url: @task.url_for(task_id)}
114
130
  end
115
131
 
116
132
  def iso(time)
@@ -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
- ["Failed:", :red, "✗ release phase failed (#{detail})"]
65
- else
66
- ["Releasing:", :yellow, "⟳ release phase (#{detail})"]
67
- end
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('→', :dim)} #{linkify(subject, info ? sha : nil)}"]
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 ? 'commit' : 'commits'} behind origin/#{@branch}", :yellow)
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
- Report = Data.define(:snapshot, :remote_head, :delta, :pinned, :mine, :commits) do
14
- def self.build(snapshot:, remote_head:, git:)
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
- .compact.uniq.to_h { |sha| [sha, git.commit_info(sha)] }
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
- :compute_mine, :identity_match?
103
+ :compute_mine, :identity_match?, :live_rollout
95
104
  end
96
105
  end
data/lib/onair/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Onair
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onair-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene