onair-cli 0.4.0 → 0.5.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 +11 -0
- data/README.md +65 -0
- data/lib/onair/cli.rb +3 -3
- data/lib/onair/config.rb +9 -6
- data/lib/onair/platform/base.rb +20 -4
- data/lib/onair/platform/dokku.rb +1 -0
- data/lib/onair/platform/github_deployments.rb +157 -0
- data/lib/onair/platform/heroku.rb +5 -0
- data/lib/onair/renderer/json.rb +3 -1
- data/lib/onair/renderer/tty.rb +20 -5
- data/lib/onair/version.rb +1 -1
- data/lib/onair.rb +1 -0
- 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: 863a5cc9f6796e525c125ed320542a3e4d8b076b5a6a5660744c9136ead84cc2
|
|
4
|
+
data.tar.gz: c771ccb2bd0c72fcf075e48c829556de327ad4dc3b2527db4ca0b5b0ee763e34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8016ad8033b28ec15e73cdae96b1b0e94562a4bc28226dfd5b556c8a3a24dc713106b9f8fe5b3c3881c330e71f9a297c768c0a2c9175114eccfdd4cc4eae76d
|
|
7
|
+
data.tar.gz: f78628cf04574876bc46322b9eba02d8f252152c90d7bc75bd032041be4a9f3741f6f3619d01693a3f9f098e18fcd64c6e14e722c27e65d51640a1bf5e7660e7
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-11-02
|
|
6
|
+
|
|
7
|
+
- GitHub Deployments: `platform: github_deployments` reads one environment's
|
|
8
|
+
deployments (`environment:`, default `production`) through the GitHub API
|
|
9
|
+
with the `gh` token, so anyone who can read the repo sees what is deployed
|
|
10
|
+
without host access. The newest successful deployment is Deployed, a newer
|
|
11
|
+
in-flight one is Pending with its commit, and a newer failed one is Failed.
|
|
12
|
+
It reports what CI recorded, not what the host runs.
|
|
13
|
+
- `app:` is required only by the platforms that use it. `--json` gains an
|
|
14
|
+
`environment` field, and `deployed` is `null` when nothing successful is deployed yet.
|
|
15
|
+
|
|
5
16
|
## [0.4.0] - 2026-11-02
|
|
6
17
|
|
|
7
18
|
- Dokku: `platform: dokku` with a `host:` reads the running commit from the
|
data/README.md
CHANGED
|
@@ -28,6 +28,8 @@ case.
|
|
|
28
28
|
## Principles
|
|
29
29
|
|
|
30
30
|
1. **Truth over convenience.** What's *running*, not what was built last.
|
|
31
|
+
The one exception is opt-in: `platform: github_deployments` reports what
|
|
32
|
+
CI recorded, not what the host runs (see [GitHub Deployments](#github-deployments)).
|
|
31
33
|
2. **Zero runtime dependencies.** Ruby stdlib only.
|
|
32
34
|
3. **Read-only.** Never deploys, never rolls back, never writes to git except
|
|
33
35
|
an explicit lazy `git fetch`.
|
|
@@ -123,6 +125,65 @@ another way (`git:from-image`, `git:sync`) shows the deploy source instead
|
|
|
123
125
|
of a commit. Dokku builds record no commit, so `Pending` and `Failed` name
|
|
124
126
|
the build, and pinned detection stays silent.
|
|
125
127
|
|
|
128
|
+
### GitHub Deployments
|
|
129
|
+
|
|
130
|
+
For teams whose CI deploys to a host most developers cannot reach (a Dokku
|
|
131
|
+
box behind a firewall, a VPS where only admins hold SSH keys). If CI records
|
|
132
|
+
each deploy as a GitHub Deployment, anyone who can read the repo sees what is
|
|
133
|
+
deployed, with no access to the host.
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
platform: github_deployments
|
|
137
|
+
repo: acme/widgets # default: inferred from origin
|
|
138
|
+
environment: production # default: production
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
No `app:` is needed. onair reads the environment's last 10 deployments and
|
|
142
|
+
the newest status of each through the GitHub API, with the token from
|
|
143
|
+
`GH_TOKEN`, `GITHUB_TOKEN` or `gh auth token`; without one it asks you to run
|
|
144
|
+
`gh auth login`. The token needs read access to the repo's deployments.
|
|
145
|
+
|
|
146
|
+
- **Deployed** is the newest deployment whose status is `success`.
|
|
147
|
+
- **Pending** is a newer deployment that is `queued`, `pending` or
|
|
148
|
+
`in_progress`, with its commit.
|
|
149
|
+
- **Failed** is a newer deployment whose status is `failure` or `error`.
|
|
150
|
+
- A deployment with no status yet is ignored.
|
|
151
|
+
- With no successful deployment among the last 10, the report says so in one
|
|
152
|
+
line, under any Pending or Failed row.
|
|
153
|
+
- If the status of a deployment newer than the deployed one cannot be read,
|
|
154
|
+
the report fails rather than show an older commit as running.
|
|
155
|
+
|
|
156
|
+
**This adapter reports what CI recorded, not what the host runs.** A deploy
|
|
157
|
+
that bypassed CI stays invisible until the next recorded one, and a CI job
|
|
158
|
+
that records the sha it pushed reports that sha even if the host ended up
|
|
159
|
+
running something else. So have CI record the sha it reads back from the host
|
|
160
|
+
after the deploy, not the sha it pushed. For example, in GitHub Actions:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
permissions:
|
|
164
|
+
deployments: write
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/checkout@v4
|
|
167
|
+
- name: Deploy
|
|
168
|
+
env:
|
|
169
|
+
GH_TOKEN: ${{ github.token }}
|
|
170
|
+
run: |
|
|
171
|
+
id=$(gh api repos/${{ github.repository }}/deployments \
|
|
172
|
+
-f ref=${{ github.sha }} -f environment=production \
|
|
173
|
+
-F auto_merge=false -f 'required_contexts[]' --jq .id)
|
|
174
|
+
gh api repos/${{ github.repository }}/deployments/$id/statuses -f state=in_progress
|
|
175
|
+
if ./deploy.sh && [ "$(./running-sha.sh)" = "${{ github.sha }}" ]; then
|
|
176
|
+
state=success
|
|
177
|
+
else
|
|
178
|
+
state=failure
|
|
179
|
+
fi
|
|
180
|
+
gh api repos/${{ github.repository }}/deployments/$id/statuses -f state=$state
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Here `deploy.sh` deploys and `running-sha.sh` asks the host which commit it
|
|
184
|
+
runs. When the host runs a different commit than the one pushed, the job
|
|
185
|
+
records a failure rather than a false success.
|
|
186
|
+
|
|
126
187
|
### Task links (optional)
|
|
127
188
|
|
|
128
189
|
If your commit subjects carry task ids (Jira, Linear, Notion, anything), tell
|
|
@@ -149,6 +210,7 @@ additively:
|
|
|
149
210
|
"platform": "heroku",
|
|
150
211
|
"branch": "main",
|
|
151
212
|
"repo": "acme/widgets",
|
|
213
|
+
"environment": null,
|
|
152
214
|
"remote_head": "<sha>",
|
|
153
215
|
"deployed": {
|
|
154
216
|
"sha": "<sha>", "version": 1234, "description": "Deploy a1b2c3d4",
|
|
@@ -171,6 +233,9 @@ additively:
|
|
|
171
233
|
```
|
|
172
234
|
|
|
173
235
|
- `delta.status` is `"current"`, `"behind"`, or `"unknown"`.
|
|
236
|
+
- `environment` is the GitHub Deployments environment read, `null` on other
|
|
237
|
+
platforms. With `platform: github_deployments`, `app` is `null`, and
|
|
238
|
+
`deployed` is `null` when no deployment among the last 10 succeeded.
|
|
174
239
|
- `pending`: `{ "sha", "build_id", "started_at", "subject", "author" }` when
|
|
175
240
|
a build is in flight. On Dokku `sha` is `null` and `build_id` names the
|
|
176
241
|
build; on Heroku `build_id` is `null`.
|
data/lib/onair/cli.rb
CHANGED
|
@@ -62,17 +62,17 @@ module Onair
|
|
|
62
62
|
def status(flags)
|
|
63
63
|
config = Config.resolve(flags)
|
|
64
64
|
git = Git.new(fetch_allowed: config.fetch)
|
|
65
|
-
adapter = Platform.build(config)
|
|
66
65
|
repo = config.repo || git.origin_repo
|
|
66
|
+
adapter = Platform.build(config, repo: repo)
|
|
67
67
|
now = Time.now
|
|
68
68
|
report = Orchestrator.new(config: config, adapter: adapter, git: git, repo: repo, now: now).run
|
|
69
69
|
|
|
70
70
|
if flags[:json]
|
|
71
71
|
puts Renderer::Json.new(report: report, app: config.app, platform: config.platform,
|
|
72
|
-
branch: config.branch, repo: repo, task: config.task).render
|
|
72
|
+
branch: config.branch, repo: repo, environment: adapter.environment, task: config.task).render
|
|
73
73
|
else
|
|
74
74
|
tty = $stdout.tty?
|
|
75
|
-
print Renderer::Tty.new(report: report, app:
|
|
75
|
+
print Renderer::Tty.new(report: report, app: adapter.target_label, platform_label: adapter.display_name,
|
|
76
76
|
branch: config.branch, repo: repo, task: config.task,
|
|
77
77
|
color: tty && !no_color?, hyperlinks: tty, now: now).render
|
|
78
78
|
end
|
data/lib/onair/config.rb
CHANGED
|
@@ -8,13 +8,16 @@ module Onair
|
|
|
8
8
|
class Config
|
|
9
9
|
FILENAME = ".onair.yml"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
DEFAULT_ENVIRONMENT = "production"
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
attr_reader :platform, :app, :repo, :branch, :fetch, :task, :host, :environment
|
|
14
|
+
|
|
15
|
+
# `app`, `host` and `environment` are each read by the adapters that need
|
|
16
|
+
# them; those adapters check them, so any may be nil elsewhere.
|
|
17
|
+
def initialize(platform:, app:, repo:, branch:, fetch:, task: nil, host: nil, environment: nil)
|
|
16
18
|
@platform = platform
|
|
17
19
|
@host = host
|
|
20
|
+
@environment = environment
|
|
18
21
|
@app = app
|
|
19
22
|
@repo = repo
|
|
20
23
|
@branch = branch
|
|
@@ -27,7 +30,6 @@ module Onair
|
|
|
27
30
|
platform = file["platform"] || "heroku"
|
|
28
31
|
# HEROKU_APP names a Heroku app, so it must not redirect another platform.
|
|
29
32
|
app = flags[:app] || ((platform == "heroku") ? env["HEROKU_APP"] : nil) || file["app"]
|
|
30
|
-
raise Error, "no app configured - pass --app NAME, set HEROKU_APP, or run `onair init`" if app.nil?
|
|
31
33
|
|
|
32
34
|
new(
|
|
33
35
|
platform: platform,
|
|
@@ -36,7 +38,8 @@ module Onair
|
|
|
36
38
|
branch: file["branch"] || "main",
|
|
37
39
|
fetch: !flags[:no_fetch],
|
|
38
40
|
task: TaskLink.from_config(file["task"]),
|
|
39
|
-
host: file["host"]
|
|
41
|
+
host: file["host"],
|
|
42
|
+
environment: file["environment"] || DEFAULT_ENVIRONMENT
|
|
40
43
|
)
|
|
41
44
|
end
|
|
42
45
|
|
data/lib/onair/platform/base.rb
CHANGED
|
@@ -9,21 +9,23 @@ module Onair
|
|
|
9
9
|
@registry[name] = klass
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def build(config)
|
|
12
|
+
def build(config, repo: nil)
|
|
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
|
-
klass.new(config)
|
|
19
|
+
klass.new(config, repo: repo)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Adapter contract: implement #snapshot returning a Snapshot.
|
|
24
24
|
#
|
|
25
25
|
# deployed: the release CURRENTLY RUNNING (nil sha only if
|
|
26
|
-
# truly unresolvable)
|
|
26
|
+
# truly unresolvable), or nil when the platform
|
|
27
|
+
# records no successful deploy; renderers then show
|
|
28
|
+
# one line in its place
|
|
27
29
|
# pending: newest in-flight build, or nil
|
|
28
30
|
# release: newest release above the running one that is not
|
|
29
31
|
# running (release phase pending or failed), or nil
|
|
@@ -40,8 +42,12 @@ module Onair
|
|
|
40
42
|
# (delta, pinned, mine, rendering) is platform-agnostic and must not
|
|
41
43
|
# reference any specific platform.
|
|
42
44
|
class Base
|
|
43
|
-
|
|
45
|
+
NO_APP = "no app configured - pass --app NAME, set HEROKU_APP, or run `onair init`"
|
|
46
|
+
|
|
47
|
+
# `repo` is the GitHub repo (config or origin remote), or nil.
|
|
48
|
+
def initialize(config, repo: nil)
|
|
44
49
|
@config = config
|
|
50
|
+
@repo = repo
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
def snapshot
|
|
@@ -52,6 +58,16 @@ module Onair
|
|
|
52
58
|
self.class.name.split("::").last
|
|
53
59
|
end
|
|
54
60
|
|
|
61
|
+
# What the report's header names next to the platform.
|
|
62
|
+
def target_label
|
|
63
|
+
@config.app
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The deploy environment the adapter reads, for platforms that have one.
|
|
67
|
+
def environment
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
55
71
|
private
|
|
56
72
|
|
|
57
73
|
# Exceptions surface via #value; without this the dying thread also
|
data/lib/onair/platform/dokku.rb
CHANGED
|
@@ -58,6 +58,7 @@ module Onair
|
|
|
58
58
|
def validate!
|
|
59
59
|
raise Error, "no host configured - add `host:` to #{Config::FILENAME}" if host.to_s.empty?
|
|
60
60
|
raise Error, "invalid Dokku host #{host.inspect}" if host.start_with?("-")
|
|
61
|
+
raise Error, NO_APP if app.nil?
|
|
61
62
|
raise Error, "invalid Dokku app name #{app.inspect}" unless APP_NAME.match?(app)
|
|
62
63
|
end
|
|
63
64
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Onair
|
|
9
|
+
module Platform
|
|
10
|
+
# Reads what CI recorded as GitHub Deployments for one environment, so
|
|
11
|
+
# anyone who can read the repo sees production without host access. It
|
|
12
|
+
# reports CI's record, not what the host runs.
|
|
13
|
+
class GithubDeployments < Base
|
|
14
|
+
HOST = "api.github.com"
|
|
15
|
+
API_VERSION = "2022-11-28"
|
|
16
|
+
WINDOW = 10
|
|
17
|
+
# owner/name, with no all-dot segment, since the repo goes into a URL path.
|
|
18
|
+
REPO_NAME = %r{\A(?!\.+/)[\w.-]+/(?!\.+\z)[\w.-]+\z}
|
|
19
|
+
SUCCESS = "success"
|
|
20
|
+
# GitHub marks an older successful deployment inactive once a newer one
|
|
21
|
+
# in the environment succeeds, so both states count as deployed once.
|
|
22
|
+
SUCCEEDED_STATES = %w[success inactive].freeze
|
|
23
|
+
IN_FLIGHT_STATES = %w[queued pending in_progress].freeze
|
|
24
|
+
FAILED_STATES = %w[failure error].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(config, repo: nil)
|
|
27
|
+
super
|
|
28
|
+
raise Error, "no GitHub repo - add `repo: owner/name` to #{Config::FILENAME}" if @repo.to_s.empty?
|
|
29
|
+
raise Error, "invalid GitHub repo #{@repo.inspect} - expected owner/name" unless REPO_NAME.match?(@repo)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def snapshot
|
|
33
|
+
token = resolve_token
|
|
34
|
+
rows = with_statuses(token, deployments(token))
|
|
35
|
+
# A deployment whose status could not be read may be the one running,
|
|
36
|
+
# so any such row above the newest success fails the report.
|
|
37
|
+
index = rows.index { |row| row[:error] || row[:state] == SUCCESS }
|
|
38
|
+
raise rows[index][:error] if index && rows[index][:error]
|
|
39
|
+
|
|
40
|
+
newer = index ? rows.first(index) : rows
|
|
41
|
+
succeeded = rows.select { |row| SUCCEEDED_STATES.include?(row[:state]) }.map { |row| row[:sha] }.uniq
|
|
42
|
+
Snapshot.new(deployed: index && deployed(rows[index]), pending: pending(newer), release: failed(newer),
|
|
43
|
+
latest_built_sha: succeeded.first, succeeded_shas: succeeded, rollout: nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def display_name
|
|
47
|
+
"GitHub"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def target_label
|
|
51
|
+
"#{@repo} (#{environment})"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def environment
|
|
55
|
+
@config.environment || Config::DEFAULT_ENVIRONMENT
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def resolve_token
|
|
61
|
+
token = Auth::GithubToken.token
|
|
62
|
+
raise Error, "no GitHub token found - run `gh auth login`" if token.nil? || token.empty?
|
|
63
|
+
|
|
64
|
+
token
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def deployments(token)
|
|
68
|
+
query = URI.encode_www_form(environment: environment, per_page: WINDOW)
|
|
69
|
+
with_http { |http| get(http, token, "/repos/#{@repo}/deployments?#{query}") }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pairs each deployment with its newest status, newest deployment first.
|
|
73
|
+
# A failed statuses call keeps its row with the error, for #snapshot to
|
|
74
|
+
# judge; a deployment with no status yet says nothing, so it is skipped.
|
|
75
|
+
def with_statuses(token, deployments)
|
|
76
|
+
threads = deployments.map do |deployment|
|
|
77
|
+
quiet_thread do
|
|
78
|
+
newest_status(token, deployment["id"])
|
|
79
|
+
rescue Error => e
|
|
80
|
+
e
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
deployments.zip(threads.map(&:value)).filter_map do |deployment, status|
|
|
84
|
+
next if status.nil?
|
|
85
|
+
|
|
86
|
+
row = {id: deployment["id"], sha: deployment["sha"], created_at: parse_time(deployment["created_at"])}
|
|
87
|
+
next row.merge(error: status) if status.is_a?(Error)
|
|
88
|
+
|
|
89
|
+
row.merge(state: status["state"], status_at: parse_time(status["created_at"]))
|
|
90
|
+
end.sort_by { |row| row[:created_at] || Time.at(0) }.reverse
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def newest_status(token, id)
|
|
94
|
+
with_http { |http| get(http, token, "/repos/#{@repo}/deployments/#{id}/statuses?per_page=1") }.first
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def deployed(row)
|
|
98
|
+
Deployed.new(sha: row[:sha], version: nil, description: "deployment #{row[:id]}",
|
|
99
|
+
deployed_at: row[:status_at])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pending(newer)
|
|
103
|
+
row = newer.find { |candidate| IN_FLIGHT_STATES.include?(candidate[:state]) }
|
|
104
|
+
row && Pending.new(sha: row[:sha], started_at: row[:created_at])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def failed(newer)
|
|
108
|
+
row = newer.find { |candidate| FAILED_STATES.include?(candidate[:state]) }
|
|
109
|
+
row && Release.new(sha: row[:sha], version: nil, description: "deployment #{row[:id]}",
|
|
110
|
+
status: :failed, started_at: row[:status_at])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def with_http(&)
|
|
114
|
+
Net::HTTP.start(HOST, 443, use_ssl: true, open_timeout: 5, read_timeout: 15, &)
|
|
115
|
+
rescue Net::OpenTimeout, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
|
|
116
|
+
raise Error, "GitHub API request failed: #{e.message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def get(http, token, path)
|
|
120
|
+
request = Net::HTTP::Get.new(path)
|
|
121
|
+
request["Authorization"] = "Bearer #{token}"
|
|
122
|
+
request["Accept"] = "application/vnd.github+json"
|
|
123
|
+
request["X-GitHub-Api-Version"] = API_VERSION
|
|
124
|
+
response = http.request(request)
|
|
125
|
+
raise Error, error_message(response, path) unless response.is_a?(Net::HTTPSuccess)
|
|
126
|
+
|
|
127
|
+
JSON.parse(response.body)
|
|
128
|
+
rescue JSON::ParserError
|
|
129
|
+
raise Error, "GitHub API returned invalid JSON for #{path}"
|
|
130
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
|
|
131
|
+
raise Error, "GitHub API request failed: #{e.message}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def error_message(response, path)
|
|
135
|
+
case response.code.to_i
|
|
136
|
+
when 401 then "GitHub rejected the token (401) - run `gh auth login`"
|
|
137
|
+
when 404 then "GitHub repo not found or not visible to this token: #{@repo}"
|
|
138
|
+
when 403, 429
|
|
139
|
+
if response["x-ratelimit-remaining"] == "0"
|
|
140
|
+
"GitHub API rate limit reached - try again later"
|
|
141
|
+
else
|
|
142
|
+
"GitHub API returned #{response.code} for #{path}"
|
|
143
|
+
end
|
|
144
|
+
else "GitHub API returned #{response.code} for #{path}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_time(value)
|
|
149
|
+
value && Time.iso8601(value)
|
|
150
|
+
rescue ArgumentError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Platform.register("github_deployments", self)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -18,6 +18,11 @@ module Onair
|
|
|
18
18
|
PREBOOT_HANDOFF = 180
|
|
19
19
|
PREBOOT_NOTE = "preboot: previous web dynos may still serve"
|
|
20
20
|
|
|
21
|
+
def initialize(config, repo: nil)
|
|
22
|
+
super
|
|
23
|
+
raise Error, NO_APP if app.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
21
26
|
def snapshot
|
|
22
27
|
token = resolve_token
|
|
23
28
|
releases_thread = quiet_thread { releases(token) }
|
data/lib/onair/renderer/json.rb
CHANGED
|
@@ -8,8 +8,9 @@ module Onair
|
|
|
8
8
|
# Machine-readable status. The schema is a public API documented in the
|
|
9
9
|
# README — additive changes only.
|
|
10
10
|
class Json
|
|
11
|
-
def initialize(report:, app:, platform:, branch:, repo:, task: nil)
|
|
11
|
+
def initialize(report:, app:, platform:, branch:, repo:, environment: nil, task: nil)
|
|
12
12
|
@report = report
|
|
13
|
+
@environment = environment
|
|
13
14
|
@app = app
|
|
14
15
|
@platform = platform
|
|
15
16
|
@branch = branch
|
|
@@ -27,6 +28,7 @@ module Onair
|
|
|
27
28
|
platform: @platform,
|
|
28
29
|
branch: @branch,
|
|
29
30
|
repo: @repo,
|
|
31
|
+
environment: @environment,
|
|
30
32
|
remote_head: @report.remote_head,
|
|
31
33
|
deployed: deployed_payload,
|
|
32
34
|
pending: pending_payload,
|
data/lib/onair/renderer/tty.rb
CHANGED
|
@@ -65,14 +65,27 @@ module Onair
|
|
|
65
65
|
return [] if release.nil?
|
|
66
66
|
return build_failed_lines(release) if release.sha.nil?
|
|
67
67
|
|
|
68
|
+
label, color, note = release.version.nil? ? versionless_release(release) : versioned_release(release)
|
|
69
|
+
row_lines(label, color, release.sha, age(release.started_at), extra: paint(note, color)) + [""]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def versioned_release(release)
|
|
68
73
|
detail = version_label(release.version)
|
|
69
74
|
detail = [detail, release.description].compact.join(": ") if release.sha == snapshot.deployed&.sha
|
|
70
|
-
|
|
75
|
+
if release.status == :failed
|
|
71
76
|
["Failed:", :red, "✗ release phase failed (#{detail})"]
|
|
72
77
|
else
|
|
73
78
|
["Releasing:", :yellow, "⟳ release phase (#{detail})"]
|
|
74
79
|
end
|
|
75
|
-
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A platform that numbers no releases names the deploy by its description.
|
|
83
|
+
def versionless_release(release)
|
|
84
|
+
if release.status == :failed
|
|
85
|
+
["Failed:", :red, "✗ deploy failed (#{release.description})"]
|
|
86
|
+
else
|
|
87
|
+
["Releasing:", :yellow, "⟳ deploying (#{release.description})"]
|
|
88
|
+
end
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
# A failed build whose commit the platform does not record.
|
|
@@ -83,15 +96,17 @@ module Onair
|
|
|
83
96
|
|
|
84
97
|
def deployed_lines
|
|
85
98
|
deployed = snapshot.deployed
|
|
86
|
-
if deployed
|
|
99
|
+
return [" #{paint("No successful deploy recorded.", :yellow)}"] if deployed.nil?
|
|
100
|
+
|
|
101
|
+
if deployed.sha
|
|
87
102
|
lines = row_lines("Deployed:", :green, deployed.sha, age(deployed.deployed_at), extra: delta_text)
|
|
88
103
|
lines << rollout_line if @report.rollout
|
|
89
104
|
lines << pinned_line if @report.pinned
|
|
90
105
|
lines.concat(yours_lines)
|
|
91
106
|
lines
|
|
92
107
|
else
|
|
93
|
-
subject = ["the running commit", version_label(deployed
|
|
94
|
-
[" #{paint("Could not resolve #{subject} (#{deployed
|
|
108
|
+
subject = ["the running commit", version_label(deployed.version)].compact.join(" for ")
|
|
109
|
+
[" #{paint("Could not resolve #{subject} (#{deployed.description}).", :yellow)}"]
|
|
95
110
|
end
|
|
96
111
|
end
|
|
97
112
|
|
data/lib/onair/version.rb
CHANGED
data/lib/onair.rb
CHANGED
|
@@ -64,6 +64,7 @@ require_relative "onair/remote_head"
|
|
|
64
64
|
require_relative "onair/platform/base"
|
|
65
65
|
require_relative "onair/platform/heroku"
|
|
66
66
|
require_relative "onair/platform/dokku"
|
|
67
|
+
require_relative "onair/platform/github_deployments"
|
|
67
68
|
require_relative "onair/renderer/tty"
|
|
68
69
|
require_relative "onair/renderer/json"
|
|
69
70
|
require_relative "onair/cli"
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eugene
|
|
@@ -32,6 +32,7 @@ files:
|
|
|
32
32
|
- lib/onair/orchestrator.rb
|
|
33
33
|
- lib/onair/platform/base.rb
|
|
34
34
|
- lib/onair/platform/dokku.rb
|
|
35
|
+
- lib/onair/platform/github_deployments.rb
|
|
35
36
|
- lib/onair/platform/heroku.rb
|
|
36
37
|
- lib/onair/remote_head.rb
|
|
37
38
|
- lib/onair/renderer/json.rb
|