omaship 0.2.1
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 +7 -0
- data/CHANGELOG.md +32 -0
- data/README.md +140 -0
- data/bin/omaship +11 -0
- data/lib/omaship/api_client.rb +89 -0
- data/lib/omaship/cli.rb +700 -0
- data/lib/omaship/credentials.rb +78 -0
- data/lib/omaship/progress_renderer.rb +15 -0
- data/lib/omaship/ship_detector.rb +47 -0
- data/lib/omaship/version.rb +3 -0
- data/lib/omaship.rb +9 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 17a560c4acd3fcb2a3ddc1f2dc878d7fa3a3bfce501821cd9ad2a0aaf6b5a0f8
|
|
4
|
+
data.tar.gz: e07f76c6417e4714473a5a699c3b3169e4934d3d5b38df61ca742d17a25fa786
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d0724a6d3f68a050d3f24bb131c4ff17965f6d992d8555288afbadb5472a6f7116c3c196d5179258fbb8214a1057e9c2fac73fcb8d110a67b5c25243538f705
|
|
7
|
+
data.tar.gz: de10cb82315db0621c4b612477391c20344535de035652d9374bbac0f3e6b60f78a9d4f4e2e7b26871e3232d26b7325d4df1e5c8cc617f742ca9814c371fb165
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.1](https://github.com/bloomedai/omaship/compare/omaship/v0.2.0...omaship/v0.2.1) (2026-03-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 🐛 enable trusted publishing for cli gem publish ([9e5a9ff](https://github.com/bloomedai/omaship/commit/9e5a9ff1c529a89a323db4b71282deb787251a3c))
|
|
9
|
+
|
|
10
|
+
## [0.2.0](https://github.com/bloomedai/omaship/compare/omaship-v0.1.0...omaship/v0.2.0) (2026-03-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* ✨ ship cli auth mvp, consent controls, and provisioning hardening ([e8b2fd0](https://github.com/bloomedai/omaship/commit/e8b2fd0f9e06e746886a666e7f1a7bd247ac1e35))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Code Refactoring
|
|
19
|
+
|
|
20
|
+
* ♻️ migrate CLAUDE.md references to AGENTS.md across codebase ([1d2323c](https://github.com/bloomedai/omaship/commit/1d2323c0b20e5cc255ca71c412bb0ad30431e75a))
|
|
21
|
+
* ♻️ Rename Project → Ship + add Infrastructure model [skip preview] ([#201](https://github.com/bloomedai/omaship/issues/201)) ([6541804](https://github.com/bloomedai/omaship/commit/65418042f501f350ea14a57234108872222c93c5))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
* 📝 update readmes for local proxy fallback and deploy workflows ([1683d5e](https://github.com/bloomedai/omaship/commit/1683d5ed801428c75a49d82b5745d85e4493e365))
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-03-04
|
|
29
|
+
|
|
30
|
+
- Initial public CLI packaging as the `omaship` gem.
|
|
31
|
+
- Added Thor-based commands for `login`, `new`, `configure`, `deploy`, and `whoami`.
|
|
32
|
+
- Added credentials storage and API client support for `/api/v1/cli`.
|
data/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Omaship CLI
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
RubyGems:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install omaship
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Homebrew:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew tap bloomedai/homebrew-tap
|
|
15
|
+
brew install omaship
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
omaship login --token <token> --host https://build.omaship.com
|
|
22
|
+
omaship list
|
|
23
|
+
omaship use <ship-ref>
|
|
24
|
+
omaship info
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Examples for `SHIP_REF` (from `omaship list` output):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
omaship use omaship/acme
|
|
31
|
+
omaship use 17
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Ship Selection
|
|
35
|
+
|
|
36
|
+
`info`, `configure`, and `deploy` resolve the target ship in this order:
|
|
37
|
+
|
|
38
|
+
1. `--ship <ship-ref>`
|
|
39
|
+
2. saved default from `omaship use <ship-ref>`
|
|
40
|
+
3. if exactly one accessible ship exists: auto-select it and persist it as default
|
|
41
|
+
4. otherwise: command fails and asks you to run `omaship list` + `omaship use ...`
|
|
42
|
+
|
|
43
|
+
Preferred ship reference format is full ship name (`org/repo`).
|
|
44
|
+
Numeric ids are also accepted.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
- `omaship login --token <token>`
|
|
49
|
+
- `omaship whoami`
|
|
50
|
+
- `omaship list`
|
|
51
|
+
- `omaship use <ship-ref>`
|
|
52
|
+
- `omaship info [--ship <ship-ref>]` (`status` and `ship` are aliases)
|
|
53
|
+
- `omaship new <name>` (requires Full CLI access)
|
|
54
|
+
- `omaship configure --payments <provider> [--ship <ship-ref>]` (requires Full CLI access)
|
|
55
|
+
- `omaship deploy [--ship <ship-ref>]` (requires Full CLI access)
|
|
56
|
+
- `omaship complete <bash|zsh|fish>` (print shell completion script)
|
|
57
|
+
- `omaship logout`
|
|
58
|
+
|
|
59
|
+
## Local Development Provisioning
|
|
60
|
+
|
|
61
|
+
When you run `omaship new <name>` against local Omaship development (`OMASHIP_HOST=http://localhost:3000`), provisioning first installs gems from `https://packages.omaship.com`.
|
|
62
|
+
|
|
63
|
+
If that registry endpoint is unavailable, Omaship automatically retries `bundle install` through the local package proxy mirror at `http://build.localhost:3000/packages`.
|
|
64
|
+
|
|
65
|
+
## Shell Completion
|
|
66
|
+
|
|
67
|
+
Bash:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mkdir -p ~/.local/share/bash-completion/completions
|
|
71
|
+
omaship complete bash > ~/.local/share/bash-completion/completions/omaship
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Zsh:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
mkdir -p ~/.zfunc
|
|
78
|
+
omaship complete zsh > ~/.zfunc/_omaship
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then ensure `~/.zfunc` is in `fpath` and reload completions:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
autoload -Uz compinit
|
|
85
|
+
compinit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Fish:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mkdir -p ~/.config/fish/completions
|
|
92
|
+
omaship complete fish > ~/.config/fish/completions/omaship.fish
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Environment
|
|
96
|
+
|
|
97
|
+
- `OMASHIP_HOST` (default: `http://localhost:3000`)
|
|
98
|
+
- `OMASHIP_TOKEN` (optional alternative for `login`)
|
|
99
|
+
|
|
100
|
+
## Credentials
|
|
101
|
+
|
|
102
|
+
- Stored at `~/.config/omaship/credentials.json`
|
|
103
|
+
- File mode is `0600`
|
|
104
|
+
- Includes host, token, user, and optional `default_ship`
|
|
105
|
+
|
|
106
|
+
## Local Development
|
|
107
|
+
|
|
108
|
+
Run from repository root:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
mise exec ruby@4.0.1 -- env BUNDLE_GEMFILE=cli/Gemfile bundle exec ruby cli/bin/omaship -h
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Run all CLI tests:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd cli
|
|
118
|
+
mise exec ruby@4.0.1 -- bundle exec ruby -Itest -e 'Dir.glob("test/**/*_test.rb").sort.each { |f| require "./#{f}" }'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Release Automation
|
|
122
|
+
|
|
123
|
+
CLI releases are created by `release-please` (`omaship/v*` tags).
|
|
124
|
+
|
|
125
|
+
Required repository secrets:
|
|
126
|
+
|
|
127
|
+
- `RELEASE_PLEASE_PAT`
|
|
128
|
+
- `HOMEBREW_TAP_GITHUB_TOKEN` (must have push access to the tap repository)
|
|
129
|
+
|
|
130
|
+
Optional repository variable:
|
|
131
|
+
|
|
132
|
+
- `HOMEBREW_TAP_REPOSITORY` (default: `bloomedai/homebrew-tap`)
|
|
133
|
+
|
|
134
|
+
RubyGems publish uses Trusted Publishing via GitHub OIDC.
|
|
135
|
+
|
|
136
|
+
Configure a trusted publisher for gem `omaship`:
|
|
137
|
+
|
|
138
|
+
- repository owner: `bloomedai`
|
|
139
|
+
- repository name: `omaship`
|
|
140
|
+
- workflow filename: `publish-gem.yml`
|
data/bin/omaship
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/retry"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Omaship
|
|
6
|
+
class ApiClient
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class UnauthorizedError < Error; end
|
|
9
|
+
class PermissionDeniedError < Error; end
|
|
10
|
+
class NotFoundError < Error; end
|
|
11
|
+
|
|
12
|
+
def initialize(host:, token:)
|
|
13
|
+
@host = host.to_s
|
|
14
|
+
@token = token.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authenticate
|
|
18
|
+
get_json("/api/v1/cli/auth")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list_ships
|
|
22
|
+
get_json("/api/v1/cli/ships").fetch("ships")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_ship(root_domain:, visibility: "private")
|
|
26
|
+
post_json("/api/v1/cli/ships", { ship: { root_domain: root_domain, visibility: visibility } })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ship(ship_id:)
|
|
30
|
+
get_json("/api/v1/cli/ships/#{ship_id}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ship_logs(ship_id:)
|
|
34
|
+
get_json("/api/v1/cli/ships/#{ship_id}/logs")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_configuration(ship_id:, package:)
|
|
38
|
+
post_json("/api/v1/cli/ships/#{ship_id}/configurations", { configuration: { package: package } })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_deploy(ship_id:)
|
|
42
|
+
post_json("/api/v1/cli/ships/#{ship_id}/deploys", {})
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def latest_deploy(ship_id:)
|
|
46
|
+
get_json("/api/v1/cli/ships/#{ship_id}/deploys/latest")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def get_json(path)
|
|
51
|
+
parse_response(connection.get(path))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post_json(path, payload)
|
|
55
|
+
parse_response(connection.post(path) do |request|
|
|
56
|
+
request.body = JSON.dump(payload)
|
|
57
|
+
request.headers["Content-Type"] = "application/json"
|
|
58
|
+
end)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_response(response)
|
|
62
|
+
case response.status
|
|
63
|
+
when 200..299
|
|
64
|
+
if response.body.to_s.empty?
|
|
65
|
+
{}
|
|
66
|
+
else
|
|
67
|
+
JSON.parse(response.body)
|
|
68
|
+
end
|
|
69
|
+
when 401
|
|
70
|
+
raise UnauthorizedError, "Unauthorized"
|
|
71
|
+
when 403
|
|
72
|
+
raise PermissionDeniedError, "Forbidden"
|
|
73
|
+
when 404
|
|
74
|
+
raise NotFoundError, "Not found"
|
|
75
|
+
else
|
|
76
|
+
raise Error, "Request failed (#{response.status}): #{response.body}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def connection
|
|
81
|
+
@connection ||= Faraday.new(url: @host) do |faraday|
|
|
82
|
+
faraday.request :retry, max: 2, interval: 0.1
|
|
83
|
+
faraday.adapter Faraday.default_adapter
|
|
84
|
+
faraday.headers["Authorization"] = "Bearer #{@token}"
|
|
85
|
+
faraday.headers["Accept"] = "application/json"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/omaship/cli.rb
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
|
|
3
|
+
module Omaship
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
map "new" => :new_ship
|
|
6
|
+
map "status" => :info
|
|
7
|
+
map "ship" => :info
|
|
8
|
+
|
|
9
|
+
class_option :host, type: :string, desc: "API host"
|
|
10
|
+
|
|
11
|
+
def self.exit_on_failure?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.start(given_args = ARGV, config = {})
|
|
16
|
+
normalized_args = normalize_help_args(given_args)
|
|
17
|
+
super(normalized_args, config)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.normalize_help_args(given_args)
|
|
21
|
+
args = given_args.dup
|
|
22
|
+
|
|
23
|
+
if root_help_requested?(args)
|
|
24
|
+
[]
|
|
25
|
+
else
|
|
26
|
+
help_flag_index = args.index("-h") || args.index("--help")
|
|
27
|
+
if help_flag_index
|
|
28
|
+
[ "help", args.first ]
|
|
29
|
+
else
|
|
30
|
+
args
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.root_help_requested?(args)
|
|
36
|
+
args == [ "-h" ] || args == [ "--help" ] || args == [ "help" ]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.help(shell, subcommand = false)
|
|
40
|
+
if subcommand
|
|
41
|
+
super
|
|
42
|
+
else
|
|
43
|
+
shell.say "Usage:"
|
|
44
|
+
shell.say " omaship COMMAND [options]"
|
|
45
|
+
shell.say
|
|
46
|
+
shell.say "You must specify a command. The most common commands are:"
|
|
47
|
+
shell.say
|
|
48
|
+
shell.say " login Store API token credentials locally"
|
|
49
|
+
shell.say " whoami Show authenticated user"
|
|
50
|
+
shell.say " list List ships you can access"
|
|
51
|
+
shell.say " use Set default ship from `omaship list` (for example: `omaship use omaship/acme`)"
|
|
52
|
+
shell.say " info Show details for a ship (aliases: ship, status)"
|
|
53
|
+
shell.say " new Create and provision a new ship"
|
|
54
|
+
shell.say " configure Configure a ship (requires Full CLI access)"
|
|
55
|
+
shell.say " deploy Deploy a ship (requires Full CLI access)"
|
|
56
|
+
shell.say " complete Print shell completion script (bash, zsh, fish)"
|
|
57
|
+
shell.say " logout Remove local CLI credentials"
|
|
58
|
+
shell.say
|
|
59
|
+
shell.say "Global options:"
|
|
60
|
+
shell.say " --host=HOST API host"
|
|
61
|
+
shell.say
|
|
62
|
+
shell.say "All commands can be run with -h (or --help) for more information."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "login", "Store API token credentials locally"
|
|
67
|
+
method_option :token, type: :string, desc: "API token from omaship settings"
|
|
68
|
+
def login
|
|
69
|
+
token = options[:token] || ENV["OMASHIP_TOKEN"] || ask("API token:", echo: false)
|
|
70
|
+
if token.to_s.strip.empty?
|
|
71
|
+
raise Thor::Error, "Missing API token. Pass --token or set OMASHIP_TOKEN."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
user_payload = build_api_client(token: token).authenticate.fetch("user")
|
|
75
|
+
credentials.write(
|
|
76
|
+
host: resolved_host,
|
|
77
|
+
token: token,
|
|
78
|
+
user: user_payload
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
say "Logged in as #{user_payload.fetch("email_address")}"
|
|
82
|
+
say "Credentials saved to #{credentials.path}"
|
|
83
|
+
rescue Omaship::ApiClient::UnauthorizedError
|
|
84
|
+
message = "Authentication failed. Check your API token. #{existing_credentials_status_message}"
|
|
85
|
+
raise Thor::Error, message
|
|
86
|
+
rescue Omaship::ApiClient::Error => error
|
|
87
|
+
raise Thor::Error, error.message
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc "logout", "Remove local CLI credentials"
|
|
91
|
+
def logout
|
|
92
|
+
credentials.clear
|
|
93
|
+
say "Logged out."
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "whoami", "Show authenticated user"
|
|
97
|
+
def whoami
|
|
98
|
+
with_api_error_handling(command: :whoami) do
|
|
99
|
+
token = current_token
|
|
100
|
+
client = build_api_client(token: token)
|
|
101
|
+
auth_payload = client.authenticate
|
|
102
|
+
user_payload = auth_payload.fetch("user")
|
|
103
|
+
account_payload = auth_payload.fetch("account", {})
|
|
104
|
+
token_payload = auth_payload.fetch("token", {})
|
|
105
|
+
ship_count = account_payload.fetch("ship_count", 0).to_i
|
|
106
|
+
orgs = normalize_orgs(account_payload["orgs"])
|
|
107
|
+
access_level = token_payload.fetch("access_level", "unknown").to_s.strip
|
|
108
|
+
if access_level.empty?
|
|
109
|
+
access_level = "unknown"
|
|
110
|
+
end
|
|
111
|
+
default_ship_reference = credentials.default_ship.to_s.strip
|
|
112
|
+
|
|
113
|
+
say "User: #{user_payload.fetch("email_address")}"
|
|
114
|
+
say "Host: #{resolved_host}"
|
|
115
|
+
say "Access: #{access_level}"
|
|
116
|
+
say "Ships: #{ship_count}"
|
|
117
|
+
if ship_count.zero?
|
|
118
|
+
say "Orgs: none (no ships yet)"
|
|
119
|
+
elsif orgs.empty?
|
|
120
|
+
say "Orgs: unavailable"
|
|
121
|
+
else
|
|
122
|
+
say "Orgs: #{orgs.join(", ")}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if default_ship_reference.empty?
|
|
126
|
+
say "Default Ship: -"
|
|
127
|
+
else
|
|
128
|
+
say "Default Ship: #{default_ship_reference}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
desc "list", "List ships you can access"
|
|
134
|
+
def list
|
|
135
|
+
with_api_error_handling(command: :list) do
|
|
136
|
+
token = current_token
|
|
137
|
+
ships = build_api_client(token: token).list_ships
|
|
138
|
+
default_ship_reference = credentials.default_ship.to_s.strip
|
|
139
|
+
|
|
140
|
+
if ships.empty?
|
|
141
|
+
say "No ships available yet."
|
|
142
|
+
else
|
|
143
|
+
say "Ships:"
|
|
144
|
+
ships.each do |ship|
|
|
145
|
+
marker = default_ship_match?(ship: ship, ship_reference: default_ship_reference) ? "*" : " "
|
|
146
|
+
app_url = ship["app_url"] || "-"
|
|
147
|
+
say "#{marker} #{ship.fetch("id")} #{ship.fetch("full_name")} #{ship.fetch("status")} #{app_url}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if !default_ship_reference.empty?
|
|
151
|
+
say
|
|
152
|
+
say "* default ship: #{default_ship_reference}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
desc "use SHIP", "Set default ship from `omaship list`"
|
|
159
|
+
long_desc <<~DESC
|
|
160
|
+
Set the default ship used by commands when `--ship` is omitted.
|
|
161
|
+
|
|
162
|
+
Get available values with `omaship list`.
|
|
163
|
+
Preferred format is full ship name (`org/repo`), for example `omaship/acme`.
|
|
164
|
+
Numeric ship ids from `omaship list` also work.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
omaship use omaship/acme
|
|
168
|
+
omaship use 17
|
|
169
|
+
DESC
|
|
170
|
+
def use(ship_reference)
|
|
171
|
+
with_api_error_handling(command: :use) do
|
|
172
|
+
token = current_token
|
|
173
|
+
ship = find_ship_by_reference(token: token, ship_reference: ship_reference)
|
|
174
|
+
persist_default_ship(ship)
|
|
175
|
+
|
|
176
|
+
say "Default ship set to #{ship.fetch("full_name")}."
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
desc "info", "Show ship info"
|
|
181
|
+
method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
|
|
182
|
+
def info
|
|
183
|
+
with_api_error_handling(command: :info) do
|
|
184
|
+
token = current_token
|
|
185
|
+
client = build_api_client(token: token)
|
|
186
|
+
resolved_ship = resolve_ship(token: token)
|
|
187
|
+
ship = client.ship(ship_id: resolved_ship.fetch("id")).fetch("ship")
|
|
188
|
+
deploy = client.latest_deploy(ship_id: ship.fetch("id")).fetch("deploy")
|
|
189
|
+
default_ship_reference = credentials.default_ship.to_s.strip
|
|
190
|
+
|
|
191
|
+
say "Ship: #{ship.fetch("full_name")}"
|
|
192
|
+
say "ID: #{ship.fetch("id")}"
|
|
193
|
+
say "Status: #{ship.fetch("status")}"
|
|
194
|
+
say "App URL: #{ship["app_url"] || "-"}"
|
|
195
|
+
say "Repo URL: #{ship["repo_url"] || "-"}"
|
|
196
|
+
say "Default Ship: #{default_ship_reference.empty? ? "-" : default_ship_reference}"
|
|
197
|
+
say "Last Deploy: #{deploy_status_summary(deploy)}"
|
|
198
|
+
if deploy["run_number"]
|
|
199
|
+
say "Run: ##{deploy.fetch("run_number")}"
|
|
200
|
+
end
|
|
201
|
+
if deploy["started_at"]
|
|
202
|
+
say "Started At: #{deploy.fetch("started_at")}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
desc "new NAME", "Create and provision a new ship"
|
|
208
|
+
method_option :domain, type: :string, desc: "Root domain (defaults to NAME.com)"
|
|
209
|
+
def new_ship(name)
|
|
210
|
+
with_api_error_handling(command: :new_ship) do
|
|
211
|
+
token = current_token
|
|
212
|
+
root_domain = options[:domain] || "#{name}.com"
|
|
213
|
+
payload = build_api_client(token: token).create_ship(root_domain: root_domain)
|
|
214
|
+
ship = payload.fetch("ship")
|
|
215
|
+
|
|
216
|
+
progress_renderer.step("Setting up your codebase")
|
|
217
|
+
final_ship = poll_until_terminal(ship_id: ship.fetch("id"), token: token)
|
|
218
|
+
|
|
219
|
+
if final_ship.fetch("status") == "live"
|
|
220
|
+
progress_renderer.step("Ready. Customers can sign up and pay at #{final_ship.fetch("root_domain")}")
|
|
221
|
+
else
|
|
222
|
+
message = final_ship["error_message"] || "Provisioning failed"
|
|
223
|
+
raise Thor::Error, message
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
desc "configure --payments PROVIDER", "Configure a ship"
|
|
229
|
+
method_option :payments, type: :string, required: true
|
|
230
|
+
method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
|
|
231
|
+
def configure
|
|
232
|
+
with_api_error_handling(command: :configure) do
|
|
233
|
+
payments_provider = options[:payments].to_s.strip.downcase
|
|
234
|
+
|
|
235
|
+
token = current_token
|
|
236
|
+
ship = resolve_ship(token: token)
|
|
237
|
+
build_api_client(token: token).create_configuration(ship_id: ship.fetch("id"), package: payments_provider)
|
|
238
|
+
|
|
239
|
+
progress_renderer.step("Configuration queued.")
|
|
240
|
+
|
|
241
|
+
poll_until_configuration_complete(ship_id: ship.fetch("id"), token: token)
|
|
242
|
+
progress_renderer.step("Configuration applied.")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
desc "deploy", "Deploy a ship (requires Full CLI access)"
|
|
247
|
+
method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
|
|
248
|
+
def deploy
|
|
249
|
+
with_api_error_handling(command: :deploy) do
|
|
250
|
+
token = current_token
|
|
251
|
+
ship = resolve_ship_for_deploy(token: token)
|
|
252
|
+
ship_name = ship.fetch("full_name", "##{ship.fetch("id")}")
|
|
253
|
+
|
|
254
|
+
progress_renderer.step("Target ship: #{ship_name}.")
|
|
255
|
+
|
|
256
|
+
build_api_client(token: token).create_deploy(ship_id: ship.fetch("id"))
|
|
257
|
+
progress_renderer.step("Deploy requested.")
|
|
258
|
+
|
|
259
|
+
poll_until_deploy_finished(ship_id: ship.fetch("id"), token: token)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
desc "complete SHELL", "Print shell completion script (bash, zsh, fish)"
|
|
264
|
+
long_desc <<~DESC
|
|
265
|
+
Print a shell completion script to stdout.
|
|
266
|
+
|
|
267
|
+
Supported shells:
|
|
268
|
+
bash
|
|
269
|
+
zsh
|
|
270
|
+
fish
|
|
271
|
+
|
|
272
|
+
Examples:
|
|
273
|
+
omaship complete bash
|
|
274
|
+
omaship complete zsh
|
|
275
|
+
omaship complete fish
|
|
276
|
+
DESC
|
|
277
|
+
def complete(shell_name)
|
|
278
|
+
normalized_shell = shell_name.to_s.strip.downcase
|
|
279
|
+
script = completion_script_for(shell: normalized_shell)
|
|
280
|
+
say script
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
def completion_script_for(shell:)
|
|
285
|
+
if shell == "bash"
|
|
286
|
+
bash_completion_script
|
|
287
|
+
elsif shell == "zsh"
|
|
288
|
+
zsh_completion_script
|
|
289
|
+
elsif shell == "fish"
|
|
290
|
+
fish_completion_script
|
|
291
|
+
else
|
|
292
|
+
raise Thor::Error, "Unsupported shell `#{shell}`. Supported shells: bash, zsh, fish."
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def bash_completion_script
|
|
297
|
+
<<~BASH
|
|
298
|
+
_omaship() {
|
|
299
|
+
local cur prev command
|
|
300
|
+
COMPREPLY=()
|
|
301
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
302
|
+
prev=""
|
|
303
|
+
command="${COMP_WORDS[1]}"
|
|
304
|
+
|
|
305
|
+
if [[ ${COMP_CWORD} -gt 0 ]]; then
|
|
306
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
310
|
+
COMPREPLY=( $(compgen -W "login whoami list use info status ship new configure deploy logout complete help" -- "${cur}") )
|
|
311
|
+
return 0
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
if [[ "${prev}" == "complete" ]]; then
|
|
315
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
|
|
316
|
+
return 0
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
if [[ "${prev}" == "--payments" ]]; then
|
|
320
|
+
COMPREPLY=( $(compgen -W "stripe" -- "${cur}") )
|
|
321
|
+
return 0
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
if [[ "${prev}" == "--ship" ]]; then
|
|
325
|
+
COMPREPLY=( $(compgen -W "$(omaship list 2>/dev/null | awk '/^[* ] [0-9]+ / { print $3 }')" -- "${cur}") )
|
|
326
|
+
return 0
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [[ "${command}" == "use" && ${COMP_CWORD} -eq 2 ]]; then
|
|
330
|
+
COMPREPLY=( $(compgen -W "$(omaship list 2>/dev/null | awk '/^[* ] [0-9]+ / { print $3 }')" -- "${cur}") )
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
case "${command}" in
|
|
335
|
+
login)
|
|
336
|
+
COMPREPLY=( $(compgen -W "--token --host -h --help" -- "${cur}") )
|
|
337
|
+
;;
|
|
338
|
+
whoami|list|logout)
|
|
339
|
+
COMPREPLY=( $(compgen -W "--host -h --help" -- "${cur}") )
|
|
340
|
+
;;
|
|
341
|
+
use)
|
|
342
|
+
COMPREPLY=( $(compgen -W "--host -h --help" -- "${cur}") )
|
|
343
|
+
;;
|
|
344
|
+
info|status|ship|deploy)
|
|
345
|
+
COMPREPLY=( $(compgen -W "--ship --host -h --help" -- "${cur}") )
|
|
346
|
+
;;
|
|
347
|
+
new)
|
|
348
|
+
COMPREPLY=( $(compgen -W "--domain --host -h --help" -- "${cur}") )
|
|
349
|
+
;;
|
|
350
|
+
configure)
|
|
351
|
+
COMPREPLY=( $(compgen -W "--payments --ship --host -h --help" -- "${cur}") )
|
|
352
|
+
;;
|
|
353
|
+
complete)
|
|
354
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
|
|
355
|
+
;;
|
|
356
|
+
*)
|
|
357
|
+
COMPREPLY=( $(compgen -W "--host -h --help" -- "${cur}") )
|
|
358
|
+
;;
|
|
359
|
+
esac
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
complete -o default -F _omaship omaship
|
|
363
|
+
BASH
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def zsh_completion_script
|
|
367
|
+
<<~ZSH
|
|
368
|
+
#compdef omaship
|
|
369
|
+
|
|
370
|
+
_omaship_ship_refs() {
|
|
371
|
+
local -a refs
|
|
372
|
+
refs=(${(f)"$(omaship list 2>/dev/null | awk '/^[* ] [0-9]+ / { print $3 }')"})
|
|
373
|
+
_describe 'ship' refs
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_omaship() {
|
|
377
|
+
local context state line
|
|
378
|
+
local -a commands
|
|
379
|
+
|
|
380
|
+
commands=(
|
|
381
|
+
'login:Store API token credentials locally'
|
|
382
|
+
'whoami:Show authenticated user'
|
|
383
|
+
'list:List ships you can access'
|
|
384
|
+
'use:Set default ship from omaship list'
|
|
385
|
+
'info:Show ship info'
|
|
386
|
+
'status:Alias for info'
|
|
387
|
+
'ship:Alias for info'
|
|
388
|
+
'new:Create and provision a new ship'
|
|
389
|
+
'configure:Configure a ship'
|
|
390
|
+
'deploy:Deploy a ship'
|
|
391
|
+
'logout:Remove local CLI credentials'
|
|
392
|
+
'complete:Print shell completion script'
|
|
393
|
+
'help:Describe available commands'
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
_arguments -C \
|
|
397
|
+
'--host[API host]:host:' \
|
|
398
|
+
'1:command:->command' \
|
|
399
|
+
'*::arg:->args'
|
|
400
|
+
|
|
401
|
+
case $state in
|
|
402
|
+
command)
|
|
403
|
+
_describe -t commands 'command' commands
|
|
404
|
+
;;
|
|
405
|
+
args)
|
|
406
|
+
case $line[1] in
|
|
407
|
+
login)
|
|
408
|
+
_arguments '--token[API token from omaship settings]:token:' '--host[API host]:host:'
|
|
409
|
+
;;
|
|
410
|
+
whoami|list|logout)
|
|
411
|
+
_arguments '--host[API host]:host:'
|
|
412
|
+
;;
|
|
413
|
+
use)
|
|
414
|
+
_arguments '--host[API host]:host:' '1:ship reference:_omaship_ship_refs'
|
|
415
|
+
;;
|
|
416
|
+
info|status|ship|deploy)
|
|
417
|
+
_arguments '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
|
|
418
|
+
;;
|
|
419
|
+
new)
|
|
420
|
+
_arguments '--domain[Root domain]:domain:' '--host[API host]:host:' '1:name:'
|
|
421
|
+
;;
|
|
422
|
+
configure)
|
|
423
|
+
_arguments '--payments[Payments provider]:provider:(stripe)' '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
|
|
424
|
+
;;
|
|
425
|
+
complete)
|
|
426
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
427
|
+
;;
|
|
428
|
+
*)
|
|
429
|
+
_arguments '--host[API host]:host:'
|
|
430
|
+
;;
|
|
431
|
+
esac
|
|
432
|
+
;;
|
|
433
|
+
esac
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
compdef _omaship omaship
|
|
437
|
+
ZSH
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def fish_completion_script
|
|
441
|
+
<<~FISH
|
|
442
|
+
function __fish_omaship_ship_refs
|
|
443
|
+
omaship list 2>/dev/null | awk '/^[* ] [0-9]+ / { print $3 }'
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
complete -c omaship -f
|
|
447
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'login whoami list use info status ship new configure deploy logout complete help'
|
|
448
|
+
complete -c omaship -l host -d 'API host'
|
|
449
|
+
|
|
450
|
+
complete -c omaship -n '__fish_seen_subcommand_from login' -l token -d 'API token from omaship settings'
|
|
451
|
+
complete -c omaship -n '__fish_seen_subcommand_from new' -l domain -d 'Root domain'
|
|
452
|
+
complete -c omaship -n '__fish_seen_subcommand_from configure' -l payments -d 'Payments provider' -a 'stripe'
|
|
453
|
+
complete -c omaship -n '__fish_seen_subcommand_from configure' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
|
|
454
|
+
complete -c omaship -n '__fish_seen_subcommand_from info status ship deploy' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
|
|
455
|
+
complete -c omaship -n '__fish_seen_subcommand_from use' -f -a '(__fish_omaship_ship_refs)'
|
|
456
|
+
complete -c omaship -n '__fish_seen_subcommand_from complete' -f -a 'bash zsh fish'
|
|
457
|
+
FISH
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def current_token
|
|
461
|
+
value = credentials.token
|
|
462
|
+
if value.to_s.strip.empty?
|
|
463
|
+
raise Thor::Error, "Not logged in. Run `omaship login` first."
|
|
464
|
+
end
|
|
465
|
+
value
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def resolve_ship(token:)
|
|
469
|
+
requested_ship_reference = options[:ship].to_s.strip
|
|
470
|
+
|
|
471
|
+
if requested_ship_reference.empty?
|
|
472
|
+
resolve_ship_without_explicit_selection(token: token)
|
|
473
|
+
else
|
|
474
|
+
find_ship_by_reference(token: token, ship_reference: requested_ship_reference)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def resolve_ship_without_explicit_selection(token:)
|
|
479
|
+
ships = build_api_client(token: token).list_ships
|
|
480
|
+
default_ship_reference = credentials.default_ship.to_s.strip
|
|
481
|
+
|
|
482
|
+
if !default_ship_reference.empty?
|
|
483
|
+
default_ship = match_ship(ships: ships, ship_reference: default_ship_reference)
|
|
484
|
+
|
|
485
|
+
if default_ship
|
|
486
|
+
return default_ship
|
|
487
|
+
else
|
|
488
|
+
credentials.clear_default_ship
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
if ships.empty?
|
|
493
|
+
raise Thor::Error, no_ships_message
|
|
494
|
+
elsif ships.one?
|
|
495
|
+
ship = ships.first
|
|
496
|
+
persist_default_ship(ship)
|
|
497
|
+
ship
|
|
498
|
+
else
|
|
499
|
+
raise Thor::Error, multiple_ships_message
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def find_ship_by_reference(token:, ship_reference:)
|
|
504
|
+
ships = build_api_client(token: token).list_ships
|
|
505
|
+
ship = match_ship(ships: ships, ship_reference: ship_reference)
|
|
506
|
+
|
|
507
|
+
if ship
|
|
508
|
+
ship
|
|
509
|
+
else
|
|
510
|
+
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use the full ship name (for example: `omaship use omaship/acme`) or the numeric id."
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def match_ship(ships:, ship_reference:)
|
|
515
|
+
ships.find do |ship|
|
|
516
|
+
default_ship_match?(ship: ship, ship_reference: ship_reference)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def default_ship_match?(ship:, ship_reference:)
|
|
521
|
+
ship.fetch("id").to_s == ship_reference || ship.fetch("full_name") == ship_reference
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def persist_default_ship(ship)
|
|
525
|
+
credentials.write_default_ship(ship.fetch("full_name"))
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def resolve_ship_for_deploy(token:)
|
|
529
|
+
resolve_ship(token: token)
|
|
530
|
+
rescue Thor::Error => error
|
|
531
|
+
if options[:ship].to_s.strip.empty? && selection_error_message?(error.message)
|
|
532
|
+
raise Thor::Error, "#{error.message} `omaship deploy` also requires a full-access token."
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
raise
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def selection_error_message?(message)
|
|
539
|
+
message == no_ships_message || message == multiple_ships_message
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def no_ships_message
|
|
543
|
+
"No ships available yet. Run `omaship new <name>` first."
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def multiple_ships_message
|
|
547
|
+
"Multiple ships found. Run `omaship list` and `omaship use <org/repo>` (or a ship id)."
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def with_api_error_handling(command:)
|
|
551
|
+
yield
|
|
552
|
+
rescue Omaship::ApiClient::UnauthorizedError
|
|
553
|
+
raise Thor::Error, "Authentication failed. Check your API token and run `omaship login` again."
|
|
554
|
+
rescue Omaship::ApiClient::PermissionDeniedError
|
|
555
|
+
raise Thor::Error, permission_denied_message(command: command)
|
|
556
|
+
rescue Omaship::ApiClient::Error => error
|
|
557
|
+
raise Thor::Error, error.message
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def permission_denied_message(command:)
|
|
561
|
+
if command == :new_ship
|
|
562
|
+
"`omaship new` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
563
|
+
elsif command == :configure
|
|
564
|
+
"`omaship configure` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
565
|
+
elsif command == :deploy
|
|
566
|
+
"`omaship deploy` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
567
|
+
else
|
|
568
|
+
"Permission denied for this action. Check your token scopes and run `omaship login` again."
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def build_api_client(token:)
|
|
573
|
+
Omaship::ApiClient.new(host: resolved_host, token: token)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def credentials
|
|
577
|
+
@credentials ||= Omaship::Credentials.new
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def progress_renderer
|
|
581
|
+
@progress_renderer ||= Omaship::ProgressRenderer.new(out: $stdout)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def emit_logs(log_entries)
|
|
585
|
+
log_entries.each do |entry|
|
|
586
|
+
message = entry.fetch("message")
|
|
587
|
+
progress_renderer.step(message)
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def poll_until_terminal(ship_id:, token:)
|
|
592
|
+
client = build_api_client(token: token)
|
|
593
|
+
last_log_id = 0
|
|
594
|
+
90.times do
|
|
595
|
+
ship_payload = client.ship(ship_id: ship_id).fetch("ship")
|
|
596
|
+
logs_payload = client.ship_logs(ship_id: ship_id)
|
|
597
|
+
logs = logs_payload.fetch("logs").select { |entry| entry.fetch("id") > last_log_id }
|
|
598
|
+
if logs.any?
|
|
599
|
+
last_log_id = logs.last.fetch("id")
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
emit_logs(logs)
|
|
603
|
+
status = ship_payload.fetch("status")
|
|
604
|
+
if %w[live error].include?(status)
|
|
605
|
+
return ship_payload
|
|
606
|
+
end
|
|
607
|
+
sleep 2
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
raise Thor::Error, "Provisioning timed out."
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def poll_until_configuration_complete(ship_id:, token:)
|
|
614
|
+
client = build_api_client(token: token)
|
|
615
|
+
60.times do
|
|
616
|
+
ship_payload = client.ship(ship_id: ship_id).fetch("ship")
|
|
617
|
+
pending = ship_payload.fetch("package_update_pending")
|
|
618
|
+
if pending
|
|
619
|
+
progress_renderer.step("Applying configuration changes.")
|
|
620
|
+
sleep 2
|
|
621
|
+
else
|
|
622
|
+
error_message = ship_payload["error_message"].to_s.strip
|
|
623
|
+
if error_message.empty?
|
|
624
|
+
return
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
raise Thor::Error, error_message
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
raise Thor::Error, "Configuration timed out."
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def poll_until_deploy_finished(ship_id:, token:)
|
|
635
|
+
client = build_api_client(token: token)
|
|
636
|
+
last_status = nil
|
|
637
|
+
90.times do
|
|
638
|
+
deploy_payload = client.latest_deploy(ship_id: ship_id).fetch("deploy")
|
|
639
|
+
status = deploy_payload.fetch("status")
|
|
640
|
+
conclusion = deploy_payload["conclusion"]
|
|
641
|
+
if status != last_status
|
|
642
|
+
emit_deploy_status(status: status)
|
|
643
|
+
last_status = status
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
if status == "completed"
|
|
647
|
+
if conclusion == "success"
|
|
648
|
+
progress_renderer.step("Live. 34 seconds, zero downtime.")
|
|
649
|
+
return
|
|
650
|
+
end
|
|
651
|
+
raise Thor::Error, "Deploy failed with conclusion: #{conclusion || 'unknown'}"
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
sleep 2
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
raise Thor::Error, "Deploy timed out."
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def deploy_status_summary(deploy_payload)
|
|
661
|
+
status = deploy_payload.fetch("status")
|
|
662
|
+
conclusion = deploy_payload["conclusion"].to_s.strip
|
|
663
|
+
|
|
664
|
+
if conclusion.empty?
|
|
665
|
+
status
|
|
666
|
+
else
|
|
667
|
+
"#{status} (#{conclusion})"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def emit_deploy_status(status:)
|
|
672
|
+
if status == "not_found"
|
|
673
|
+
progress_renderer.step("Waiting for deploy workflow run.")
|
|
674
|
+
elsif %w[queued requested waiting pending].include?(status)
|
|
675
|
+
progress_renderer.step("Deploy queued.")
|
|
676
|
+
elsif status == "in_progress"
|
|
677
|
+
progress_renderer.step("Deploy in progress.")
|
|
678
|
+
elsif status != "completed"
|
|
679
|
+
progress_renderer.step("Deploy status: #{status}.")
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def resolved_host
|
|
684
|
+
options[:host] || credentials.host || ENV["OMASHIP_HOST"] || "http://localhost:3000"
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def existing_credentials_status_message
|
|
688
|
+
saved_email = credentials.read.dig("user", "email_address").to_s.strip
|
|
689
|
+
if saved_email.empty?
|
|
690
|
+
"No local credentials were changed."
|
|
691
|
+
else
|
|
692
|
+
"Existing login kept for #{saved_email}."
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def normalize_orgs(orgs)
|
|
697
|
+
Array(orgs).map(&:to_s).map(&:strip).reject(&:empty?).uniq.sort
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Omaship
|
|
5
|
+
class Credentials
|
|
6
|
+
DEFAULT_PATH = File.join(Dir.home, ".config", "omaship", "credentials.json")
|
|
7
|
+
|
|
8
|
+
def initialize(path: DEFAULT_PATH)
|
|
9
|
+
@path = path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :path
|
|
13
|
+
|
|
14
|
+
def write(host:, token:, user:)
|
|
15
|
+
payload = read
|
|
16
|
+
payload["version"] = 1
|
|
17
|
+
payload["host"] = host
|
|
18
|
+
payload["token"] = token
|
|
19
|
+
payload["user"] = user
|
|
20
|
+
|
|
21
|
+
write_payload(payload)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read
|
|
25
|
+
return {} unless File.exist?(@path)
|
|
26
|
+
|
|
27
|
+
JSON.parse(File.read(@path))
|
|
28
|
+
rescue JSON::ParserError
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def token
|
|
33
|
+
read["token"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def host
|
|
37
|
+
read["host"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default_ship
|
|
41
|
+
read["default_ship"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_default_ship(ship_reference)
|
|
45
|
+
payload = read
|
|
46
|
+
payload["version"] = 1
|
|
47
|
+
payload["default_ship"] = ship_reference
|
|
48
|
+
|
|
49
|
+
write_payload(payload)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def clear_default_ship
|
|
53
|
+
payload = read
|
|
54
|
+
payload.delete("default_ship")
|
|
55
|
+
write_payload(payload)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clear
|
|
59
|
+
File.delete(@path) if File.exist?(@path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
def write_payload(payload)
|
|
64
|
+
ensure_parent_directory
|
|
65
|
+
tmp_path = "#{@path}.tmp"
|
|
66
|
+
File.write(tmp_path, JSON.pretty_generate(payload) + "\n")
|
|
67
|
+
File.chmod(0o600, tmp_path)
|
|
68
|
+
FileUtils.mv(tmp_path, @path)
|
|
69
|
+
File.chmod(0o600, @path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ensure_parent_directory
|
|
73
|
+
directory = File.dirname(@path)
|
|
74
|
+
FileUtils.mkdir_p(directory)
|
|
75
|
+
File.chmod(0o700, directory)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Omaship
|
|
4
|
+
class ShipDetector
|
|
5
|
+
class DetectionError < StandardError; end
|
|
6
|
+
SHIP_CONTEXT_ERROR_MESSAGE = "Could not determine which ship to use automatically. Pass --ship <org/repo> (or ship id) to choose a ship.".freeze
|
|
7
|
+
|
|
8
|
+
def initialize(api_client:)
|
|
9
|
+
@api_client = api_client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def detect_current_ship
|
|
13
|
+
remote = git_remote_url
|
|
14
|
+
full_name = parse_full_name(remote)
|
|
15
|
+
ships = @api_client.list_ships
|
|
16
|
+
match = ships.find { |ship| ship.fetch("full_name") == full_name }
|
|
17
|
+
|
|
18
|
+
if match
|
|
19
|
+
match
|
|
20
|
+
else
|
|
21
|
+
raise DetectionError, SHIP_CONTEXT_ERROR_MESSAGE
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
def git_remote_url
|
|
27
|
+
stdout, stderr, status = Open3.capture3("git", "remote", "get-url", "origin")
|
|
28
|
+
unless status.success?
|
|
29
|
+
raise DetectionError, SHIP_CONTEXT_ERROR_MESSAGE
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
stdout.strip
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_full_name(remote_url)
|
|
36
|
+
if remote_url.match?(/\Agit@github\.com:/)
|
|
37
|
+
remote_url.sub(/\Agit@github\.com:/, "").sub(/\.git\z/, "")
|
|
38
|
+
elsif remote_url.match?(/\Ahttps:\/\/github\.com\//)
|
|
39
|
+
remote_url.sub(/\Ahttps:\/\/github\.com\//, "").sub(/\.git\z/, "")
|
|
40
|
+
elsif remote_url.match?(/\Ahttps:\/\/[^@]+@github\.com\//)
|
|
41
|
+
remote_url.sub(/\Ahttps:\/\/[^@]+@github\.com\//, "").sub(/\.git\z/, "")
|
|
42
|
+
else
|
|
43
|
+
raise DetectionError, SHIP_CONTEXT_ERROR_MESSAGE
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/omaship.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omaship
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Omaship
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-retry
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: thor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: CLI for login, ship provisioning, configuration, and deploy operations
|
|
55
|
+
against Omaship.
|
|
56
|
+
email:
|
|
57
|
+
- hello@omaship.com
|
|
58
|
+
executables:
|
|
59
|
+
- omaship
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- README.md
|
|
65
|
+
- bin/omaship
|
|
66
|
+
- lib/omaship.rb
|
|
67
|
+
- lib/omaship/api_client.rb
|
|
68
|
+
- lib/omaship/cli.rb
|
|
69
|
+
- lib/omaship/credentials.rb
|
|
70
|
+
- lib/omaship/progress_renderer.rb
|
|
71
|
+
- lib/omaship/ship_detector.rb
|
|
72
|
+
- lib/omaship/version.rb
|
|
73
|
+
homepage: https://omaship.com
|
|
74
|
+
licenses:
|
|
75
|
+
- Nonstandard
|
|
76
|
+
metadata:
|
|
77
|
+
homepage_uri: https://omaship.com
|
|
78
|
+
source_code_uri: https://github.com/bloomedai/omaship
|
|
79
|
+
changelog_uri: https://github.com/bloomedai/omaship/blob/main/cli/CHANGELOG.md
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 4.0.0
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 4.0.3
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Omaship command-line interface
|
|
97
|
+
test_files: []
|