onair-cli 0.1.0 → 0.2.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 +8 -0
- data/README.md +21 -12
- data/lib/onair/config.rb +1 -1
- data/lib/onair/orchestrator.rb +1 -1
- data/lib/onair/platform/base.rb +2 -0
- data/lib/onair/platform/heroku.rb +39 -18
- data/lib/onair/renderer/json.rb +14 -0
- data/lib/onair/renderer/tty.rb +24 -4
- data/lib/onair/report.rb +23 -11
- data/lib/onair/version.rb +1 -1
- data/lib/onair.rb +5 -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: 1faf09e85aa982fd64a664e4d1f566c4aaf836cc3f196cde37ef9477a42c498f
|
|
4
|
+
data.tar.gz: 41404044b99b5624b947bc726cefb8e85910622f95ff31b2419bd12c612b56bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2343601742f56a59b9141dd06d597f040ca1f55eae53295dd34dad9da10127372cb544b5bf9ff6bff98fdcaf1b2d938eab0e3ea0c9309f93ebd000da9a298566
|
|
7
|
+
data.tar.gz: 4ae9c7a4348ded957fb88a922cc44d8ebebeba36f6c870303663fe922be631bed2b2ac5ea82acbfc4ac2111bffdeefc0e8ac05c60e8dbb8d9068c51a33fb1eee
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-10-07
|
|
6
|
+
|
|
7
|
+
- Heroku: Deployed is the release Heroku is serving, not the newest one. A
|
|
8
|
+
newer release still in its release phase shows as `Releasing`, a failed one
|
|
9
|
+
as `Failed`, and neither counts as on air. While such a release carries a
|
|
10
|
+
different commit, `pinned` and `★ current` stay quiet. `--json` gains a
|
|
11
|
+
`release` object.
|
|
12
|
+
|
|
5
13
|
## [0.1.0]
|
|
6
14
|
|
|
7
15
|
- Initial release: Heroku adapter with netrc auth, full behavior parity with
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
|
|
7
|
-
### See what's on air
|
|
7
|
+
### See what's on air - which commit is actually running in production.
|
|
8
8
|
|
|
9
9
|
[](https://rubygems.org/gems/onair-cli)
|
|
10
10
|
[](https://github.com/amberpixels/onair-cli/actions/workflows/ci.yml)
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
18
|
<img src="assets/demo.svg"
|
|
19
|
-
alt="onair report: a
|
|
19
|
+
alt="onair report: a release still in its release phase, the deployed release one commit behind origin/main, and your commit absorbed by the current deploy"
|
|
20
20
|
width="760">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
`onair` answers one question fast and truthfully. The deployed commit comes
|
|
24
|
-
from the running release's *slug*, never from "newest build"
|
|
24
|
+
from the running release's *slug*, never from "newest build" - after a
|
|
25
25
|
rollback those differ, and the whole point of this tool is to not lie in that
|
|
26
26
|
case.
|
|
27
27
|
|
|
@@ -33,7 +33,7 @@ case.
|
|
|
33
33
|
an explicit lazy `git fetch`.
|
|
34
34
|
4. **Platform-agnostic core.** Heroku is the first adapter, not the
|
|
35
35
|
architecture.
|
|
36
|
-
5. **Degrade gracefully.** Offline, missing commits, no color support
|
|
36
|
+
5. **Degrade gracefully.** Offline, missing commits, no color support - every
|
|
37
37
|
failure mode renders something useful, never a stack trace.
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
@@ -61,21 +61,25 @@ is not a TTY or `NO_COLOR` is set.
|
|
|
61
61
|
### Speed
|
|
62
62
|
|
|
63
63
|
The remote head of `origin/<branch>` is resolved through the GitHub API
|
|
64
|
-
(~0.4s) when a token is ambient
|
|
65
|
-
`gh` CLI
|
|
64
|
+
(~0.4s) when a token is ambient - `GH_TOKEN`, `GITHUB_TOKEN`, or a logged-in
|
|
65
|
+
`gh` CLI - with `git ls-remote` always racing alongside as the fallback
|
|
66
66
|
(no token, non-GitHub remote, API failure). Both report the live remote ref;
|
|
67
67
|
the API is a faster transport, never a cache.
|
|
68
68
|
|
|
69
69
|
### What the report shows
|
|
70
70
|
|
|
71
|
-
- **Deployed**
|
|
71
|
+
- **Deployed** - the commit the running release was built from, its age,
|
|
72
72
|
author, and a delta against `origin/main`: `★ current` or
|
|
73
73
|
`↓ N commits behind`. If the relationship is unknown (diverged history,
|
|
74
|
-
commits unavailable), no marker is shown
|
|
75
|
-
- **Pending**
|
|
76
|
-
-
|
|
74
|
+
commits unavailable), no marker is shown - silence over speculation.
|
|
75
|
+
- **Pending** - an in-flight build that's about to replace the deploy.
|
|
76
|
+
- **Releasing** - a newer release whose release phase (e.g. migrations) is
|
|
77
|
+
still running; production still serves the Deployed row until it finishes.
|
|
78
|
+
- **Failed** - the newest release failed its release phase and never went
|
|
79
|
+
live. It stays until a newer release succeeds.
|
|
80
|
+
- **⏸ pinned** - a newer build succeeded but is *not* what's running
|
|
77
81
|
(rollback / pinned release).
|
|
78
|
-
- **Yours**
|
|
82
|
+
- **Yours** - when someone else's commit is deployed but yours sits just
|
|
79
83
|
below it: your merge made it to prod, absorbed by a later deploy.
|
|
80
84
|
|
|
81
85
|
## Configuration
|
|
@@ -105,7 +109,7 @@ task:
|
|
|
105
109
|
|
|
106
110
|
Matched ids in commit subjects become clickable terminal hyperlinks (the text
|
|
107
111
|
stays identical), and `--json` gains a `task` object (`{ "id", "url" }`) per
|
|
108
|
-
commit. No `task:` section
|
|
112
|
+
commit. No `task:` section - no behavior, nothing to opt out of.
|
|
109
113
|
|
|
110
114
|
## JSON schema
|
|
111
115
|
|
|
@@ -125,6 +129,7 @@ additively:
|
|
|
125
129
|
"author": "Eugene", "task": null
|
|
126
130
|
},
|
|
127
131
|
"pending": null,
|
|
132
|
+
"release": null,
|
|
128
133
|
"delta": { "status": "current", "behind_by": 0 },
|
|
129
134
|
"pinned": null,
|
|
130
135
|
"yours": null
|
|
@@ -134,6 +139,10 @@ additively:
|
|
|
134
139
|
- `delta.status` is `"current"`, `"behind"`, or `"unknown"`.
|
|
135
140
|
- `pending`: `{ "sha", "started_at", "subject", "author" }` when a build is
|
|
136
141
|
in flight.
|
|
142
|
+
- `release`: `{ "sha", "version", "description", "status", "started_at",
|
|
143
|
+
"subject", "author" }` when a release newer than the running one is not
|
|
144
|
+
live; `status` is `"pending"` (release phase running) or `"failed"`. While
|
|
145
|
+
it carries a different sha, `delta.status` is never `"current"`.
|
|
137
146
|
- `pinned`: `{ "version", "description", "latest_built_sha" }` when a newer
|
|
138
147
|
build succeeded but is not running.
|
|
139
148
|
- `yours`: `{ "sha", "had_own_build", "subject", "author" }` when your commit
|
data/lib/onair/config.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Onair
|
|
|
22
22
|
def self.resolve(flags = {}, env: ENV, dir: Dir.pwd)
|
|
23
23
|
file = find_file(dir) || {}
|
|
24
24
|
app = flags[:app] || env["HEROKU_APP"] || file["app"]
|
|
25
|
-
raise Error, "no app configured
|
|
25
|
+
raise Error, "no app configured - pass --app NAME, set HEROKU_APP, or run `onair init`" if app.nil?
|
|
26
26
|
|
|
27
27
|
new(
|
|
28
28
|
platform: file["platform"] || "heroku",
|
data/lib/onair/orchestrator.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Onair
|
|
|
17
17
|
snapshot = snapshot_thread.value
|
|
18
18
|
remote_head = head_thread.value
|
|
19
19
|
|
|
20
|
-
needed = [snapshot.deployed&.sha, snapshot.pending&.sha, remote_head].compact
|
|
20
|
+
needed = [snapshot.deployed&.sha, snapshot.pending&.sha, snapshot.release&.sha, remote_head].compact
|
|
21
21
|
@git.fetch_once! if needed.any? { |sha| !@git.has_commit?(sha) }
|
|
22
22
|
|
|
23
23
|
Report.build(snapshot: snapshot, remote_head: remote_head, git: @git)
|
data/lib/onair/platform/base.rb
CHANGED
|
@@ -25,6 +25,8 @@ module Onair
|
|
|
25
25
|
# deployed: the release CURRENTLY RUNNING (nil sha only if
|
|
26
26
|
# truly unresolvable)
|
|
27
27
|
# pending: newest in-flight build, or nil
|
|
28
|
+
# release: newest release above the running one that is not
|
|
29
|
+
# running (release phase pending or failed), or nil
|
|
28
30
|
# latest_built_sha: newest successfully built sha, or nil
|
|
29
31
|
# succeeded_shas: recent succeeded build shas, newest first
|
|
30
32
|
#
|
|
@@ -8,14 +8,16 @@ module Onair
|
|
|
8
8
|
module Platform
|
|
9
9
|
class Heroku < Base
|
|
10
10
|
HOST = "api.heroku.com"
|
|
11
|
+
RELEASE_WINDOW = 10
|
|
12
|
+
IN_FLIGHT_STATUSES = %w[pending failed].freeze
|
|
11
13
|
|
|
12
14
|
def snapshot
|
|
13
15
|
token = resolve_token
|
|
14
|
-
|
|
16
|
+
releases_thread = quiet_thread { releases(token) }
|
|
15
17
|
builds_thread = quiet_thread { builds(token) }
|
|
16
|
-
deployed =
|
|
18
|
+
deployed, release = releases_thread.value
|
|
17
19
|
pending, succeeded_shas = builds_thread.value
|
|
18
|
-
Snapshot.new(deployed: deployed, pending: pending,
|
|
20
|
+
Snapshot.new(deployed: deployed, pending: pending, release: release,
|
|
19
21
|
latest_built_sha: succeeded_shas.first, succeeded_shas: succeeded_shas)
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -35,28 +37,47 @@ module Onair
|
|
|
35
37
|
def resolve_token
|
|
36
38
|
token = Auth::Netrc.token(HOST)
|
|
37
39
|
token = Auth::HerokuCli.token if token.nil? || token.empty?
|
|
38
|
-
raise Error, "no Heroku credentials found
|
|
40
|
+
raise Error, "no Heroku credentials found - run `heroku login`" if token.nil? || token.empty?
|
|
39
41
|
|
|
40
42
|
token
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
# The
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
45
|
+
# The running release is the one Heroku routes to (`current`), not the
|
|
46
|
+
# newest: while a release phase runs, or after it fails, the previous
|
|
47
|
+
# release keeps serving. The slug records the commit a release was built
|
|
48
|
+
# from - the only reliable source after a rollback, when the builds list
|
|
49
|
+
# still shows the newer (no-longer-running) build on top.
|
|
50
|
+
def releases(token)
|
|
47
51
|
with_http do |http|
|
|
48
|
-
|
|
49
|
-
raise Error, "no releases found for app #{app}" if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
52
|
+
rows = get(http, token, "/apps/#{app}/releases", range: "version ..; order=desc, max=#{RELEASE_WINDOW}")
|
|
53
|
+
raise Error, "no releases found for app #{app}" if rows.empty?
|
|
54
|
+
|
|
55
|
+
running = rows.find { |row| row["current"] } || rows.find { |row| row["status"] == "succeeded" }
|
|
56
|
+
raise Error, "no succeeded release among the last #{RELEASE_WINDOW} for app #{app}" if running.nil?
|
|
57
|
+
|
|
58
|
+
deployed_sha = slug_commit(http, token, running.dig("slug", "id"))
|
|
59
|
+
[deployed(running, deployed_sha), in_flight(http, token, rows.first, running, deployed_sha)]
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
63
|
+
def deployed(row, sha)
|
|
64
|
+
Deployed.new(sha: sha, version: row["version"], description: row["description"],
|
|
65
|
+
deployed_at: parse_time(row["created_at"]))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Only the newest release counts: an older failed one was superseded by
|
|
69
|
+
# whatever came after it. Without a commit there is no row to render.
|
|
70
|
+
def in_flight(http, token, newest, running, running_sha)
|
|
71
|
+
return nil if newest.equal?(running) || !IN_FLIGHT_STATUSES.include?(newest["status"])
|
|
72
|
+
|
|
73
|
+
slug_id = newest.dig("slug", "id")
|
|
74
|
+
sha = slug_id == running.dig("slug", "id") ? running_sha : slug_commit(http, token, slug_id)
|
|
75
|
+
return nil if sha.nil?
|
|
76
|
+
|
|
77
|
+
Release.new(sha: sha, version: newest["version"], description: newest["description"],
|
|
78
|
+
status: newest["status"].to_sym, started_at: parse_time(newest["created_at"]))
|
|
79
|
+
end
|
|
80
|
+
|
|
60
81
|
def slug_commit(http, token, slug_id)
|
|
61
82
|
return nil if slug_id.nil?
|
|
62
83
|
|
|
@@ -112,7 +133,7 @@ module Onair
|
|
|
112
133
|
|
|
113
134
|
def error_message(response, path)
|
|
114
135
|
case response.code.to_i
|
|
115
|
-
when 401 then "Heroku rejected the token (401)
|
|
136
|
+
when 401 then "Heroku rejected the token (401) - run `heroku login`"
|
|
116
137
|
when 404 then "Heroku app not found: #{app}"
|
|
117
138
|
else "Heroku API returned #{response.code} for #{path}"
|
|
118
139
|
end
|
data/lib/onair/renderer/json.rb
CHANGED
|
@@ -30,6 +30,7 @@ module Onair
|
|
|
30
30
|
remote_head: @report.remote_head,
|
|
31
31
|
deployed: deployed_payload,
|
|
32
32
|
pending: pending_payload,
|
|
33
|
+
release: release_payload,
|
|
33
34
|
delta: delta_payload,
|
|
34
35
|
pinned: pinned_payload,
|
|
35
36
|
yours: yours_payload
|
|
@@ -61,6 +62,19 @@ module Onair
|
|
|
61
62
|
{ sha: pending.sha, started_at: iso(pending.started_at) }.merge(commit_fields(pending.sha))
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
def release_payload
|
|
66
|
+
release = snapshot.release
|
|
67
|
+
return nil if release.nil?
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
sha: release.sha,
|
|
71
|
+
version: release.version,
|
|
72
|
+
description: release.description,
|
|
73
|
+
status: release.status.to_s,
|
|
74
|
+
started_at: iso(release.started_at)
|
|
75
|
+
}.merge(commit_fields(release.sha))
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
def delta_payload
|
|
65
79
|
case @report.delta
|
|
66
80
|
when :current then { status: "current", behind_by: 0 }
|
data/lib/onair/renderer/tty.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Onair
|
|
|
7
7
|
COLORS = {
|
|
8
8
|
bold: "\e[1m",
|
|
9
9
|
green: "\e[0;32m",
|
|
10
|
+
red: "\e[0;31m",
|
|
10
11
|
yellow: "\e[1;33m",
|
|
11
12
|
dim: "\e[2m",
|
|
12
13
|
purple: "\e[38;5;176m",
|
|
@@ -15,6 +16,7 @@ module Onair
|
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
17
18
|
NOT_FOUND_SUBJECT = "(commit not found in local git)"
|
|
19
|
+
LABEL_WIDTH = "Releasing:".length
|
|
18
20
|
|
|
19
21
|
def initialize(report:, app:, platform_label:, branch:, repo:, color:, hyperlinks:, now:, task: nil)
|
|
20
22
|
@report = report
|
|
@@ -31,6 +33,7 @@ module Onair
|
|
|
31
33
|
def render
|
|
32
34
|
lines = ["", " #{code(:purple)}#{@platform_label} #{code(:bold)}#{@app}#{code(:reset)}", ""]
|
|
33
35
|
lines.concat(pending_lines)
|
|
36
|
+
lines.concat(release_lines)
|
|
34
37
|
lines.concat(deployed_lines)
|
|
35
38
|
lines << ""
|
|
36
39
|
"#{lines.join("\n")}\n"
|
|
@@ -46,7 +49,23 @@ module Onair
|
|
|
46
49
|
pending = snapshot.pending
|
|
47
50
|
return [] if pending.nil?
|
|
48
51
|
|
|
49
|
-
row_lines("Pending:
|
|
52
|
+
row_lines("Pending:", :yellow, pending.sha, age(pending.started_at)) + [""]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Same-commit releases (config changes, rollbacks) name what they are,
|
|
56
|
+
# since the sha alone would look like the deploy below.
|
|
57
|
+
def release_lines
|
|
58
|
+
release = snapshot.release
|
|
59
|
+
return [] if release.nil?
|
|
60
|
+
|
|
61
|
+
detail = "v#{release.version}"
|
|
62
|
+
detail = "#{detail}: #{release.description}" if release.sha == snapshot.deployed&.sha
|
|
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
|
|
68
|
+
row_lines(label, color, release.sha, age(release.started_at), extra: paint(note, color)) + [""]
|
|
50
69
|
end
|
|
51
70
|
|
|
52
71
|
def deployed_lines
|
|
@@ -68,7 +87,7 @@ module Onair
|
|
|
68
87
|
|
|
69
88
|
note = mine.had_own_build ? "✓ released, then absorbed by current" : "✓ absorbed by current deploy"
|
|
70
89
|
committed_at = @report.commits[mine.sha]&.committed_at
|
|
71
|
-
[""] + row_lines("Yours:
|
|
90
|
+
[""] + row_lines("Yours:", :cyan, mine.sha, age(committed_at), extra: paint(note, :cyan))
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
def row_lines(label, color_key, sha, age, extra: nil)
|
|
@@ -76,7 +95,8 @@ module Onair
|
|
|
76
95
|
subject = info&.subject || NOT_FOUND_SUBJECT
|
|
77
96
|
author = info&.author_name || "?"
|
|
78
97
|
age_blurb = age ? "(#{age}) " : ""
|
|
79
|
-
|
|
98
|
+
padded = label.ljust(LABEL_WIDTH)
|
|
99
|
+
first = " #{paint(padded, color_key)} #{paint(sha[0, 9], :bold)} #{paint("#{age_blurb}by #{author}", :dim)}"
|
|
80
100
|
first = "#{first} #{extra}" if extra
|
|
81
101
|
[first, " #{paint('→', :dim)} #{linkify(subject, info ? sha : nil)}"]
|
|
82
102
|
end
|
|
@@ -93,7 +113,7 @@ module Onair
|
|
|
93
113
|
|
|
94
114
|
def pinned_line
|
|
95
115
|
deployed = snapshot.deployed
|
|
96
|
-
" #{paint("⏸ v#{deployed.version} (#{deployed.description})
|
|
116
|
+
" #{paint("⏸ v#{deployed.version} (#{deployed.description}) - newer build " \
|
|
97
117
|
"#{snapshot.latest_built_sha[0, 9]} succeeded but is not running", :yellow)}"
|
|
98
118
|
end
|
|
99
119
|
|
data/lib/onair/report.rb
CHANGED
|
@@ -16,15 +16,15 @@ module Onair
|
|
|
16
16
|
# stale window the newest *succeeded* build is still the previous
|
|
17
17
|
# deploy, which must not read as a rollback.
|
|
18
18
|
pinned = pinned?(snapshot)
|
|
19
|
-
snapshot =
|
|
19
|
+
snapshot = drop_superseded_pending(snapshot)
|
|
20
20
|
deployed_sha = snapshot.deployed&.sha
|
|
21
21
|
mine = compute_mine(snapshot, git)
|
|
22
|
-
commits = [deployed_sha, snapshot.pending&.sha, mine&.sha]
|
|
23
|
-
|
|
22
|
+
commits = [deployed_sha, snapshot.pending&.sha, snapshot.release&.sha, mine&.sha]
|
|
23
|
+
.compact.uniq.to_h { |sha| [sha, git.commit_info(sha)] }
|
|
24
24
|
new(
|
|
25
25
|
snapshot: snapshot,
|
|
26
26
|
remote_head: remote_head,
|
|
27
|
-
delta: compute_delta(
|
|
27
|
+
delta: compute_delta(snapshot, remote_head, git),
|
|
28
28
|
pinned: pinned,
|
|
29
29
|
mine: mine,
|
|
30
30
|
commits: commits
|
|
@@ -34,16 +34,21 @@ module Onair
|
|
|
34
34
|
# Right after a deploy finishes, the platform's builds list can still
|
|
35
35
|
# report the just-released build as pending while the releases endpoint
|
|
36
36
|
# already shows it running — the same commit would render as both
|
|
37
|
-
# Pending and Deployed. A build that is already on air isn't pending
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
# Pending and Deployed. A build that is already on air isn't pending, and
|
|
38
|
+
# one already in its release phase is shown by the release row.
|
|
39
|
+
def self.drop_superseded_pending(snapshot)
|
|
40
|
+
pending_sha = snapshot.pending&.sha
|
|
41
|
+
return snapshot unless pending_sha && [snapshot.deployed&.sha, snapshot.release&.sha].include?(pending_sha)
|
|
40
42
|
|
|
41
43
|
snapshot.with(pending: nil)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
# "Current" compares only the running sha to the head, so it stays silent
|
|
47
|
+
# while a release of a different commit is in flight or failed.
|
|
48
|
+
def self.compute_delta(snapshot, head, git)
|
|
49
|
+
sha = snapshot.deployed&.sha
|
|
45
50
|
return nil if sha.nil? || head.nil?
|
|
46
|
-
return :current if sha == head
|
|
51
|
+
return (release_of_other_commit?(snapshot) ? nil : :current) if sha == head
|
|
47
52
|
return nil unless git.has_commit?(sha) && git.has_commit?(head)
|
|
48
53
|
return nil unless git.ancestor?(sha, head)
|
|
49
54
|
|
|
@@ -51,10 +56,16 @@ module Onair
|
|
|
51
56
|
count&.positive? ? count : nil
|
|
52
57
|
end
|
|
53
58
|
|
|
59
|
+
# A release of another commit, in flight or failed, explains the newer
|
|
60
|
+
# build on its own; a same-commit one (config change) does not.
|
|
54
61
|
def self.pinned?(snapshot)
|
|
55
62
|
sha = snapshot.deployed&.sha
|
|
56
63
|
latest = snapshot.latest_built_sha
|
|
57
|
-
!sha.nil? && !latest.nil? && latest != sha && snapshot.pending.nil?
|
|
64
|
+
!sha.nil? && !latest.nil? && latest != sha && snapshot.pending.nil? && !release_of_other_commit?(snapshot)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.release_of_other_commit?(snapshot)
|
|
68
|
+
!snapshot.release.nil? && snapshot.release.sha != snapshot.deployed&.sha
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
# "Did my merge just ship?" — deliberately a 2-commit first-parent window
|
|
@@ -79,6 +90,7 @@ module Onair
|
|
|
79
90
|
(!identity.name.to_s.empty? && name == identity.name)
|
|
80
91
|
end
|
|
81
92
|
|
|
82
|
-
private_class_method :
|
|
93
|
+
private_class_method :drop_superseded_pending, :compute_delta, :pinned?, :release_of_other_commit?,
|
|
94
|
+
:compute_mine, :identity_match?
|
|
83
95
|
end
|
|
84
96
|
end
|
data/lib/onair/version.rb
CHANGED
data/lib/onair.rb
CHANGED
|
@@ -14,10 +14,14 @@ module Onair
|
|
|
14
14
|
|
|
15
15
|
Pending = Data.define(:sha, :started_at)
|
|
16
16
|
|
|
17
|
+
# A release newer than the running one that is not serving traffic: its
|
|
18
|
+
# release phase is still running (status :pending) or it failed (:failed).
|
|
19
|
+
Release = Data.define(:sha, :version, :description, :status, :started_at)
|
|
20
|
+
|
|
17
21
|
# What a platform adapter returns. `latest_built_sha` is the newest
|
|
18
22
|
# successfully built sha (rollback detection); `succeeded_shas` lists all
|
|
19
23
|
# recent succeeded build shas, newest first ("yours had its own deploy").
|
|
20
|
-
Snapshot = Data.define(:deployed, :pending, :latest_built_sha, :succeeded_shas)
|
|
24
|
+
Snapshot = Data.define(:deployed, :pending, :release, :latest_built_sha, :succeeded_shas)
|
|
21
25
|
|
|
22
26
|
Mine = Data.define(:sha, :had_own_build)
|
|
23
27
|
end
|