deployhq 2.0.4 → 2.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/bin/deployhq +3 -1
- data/lib/deploy/cli/deployment_progress_output.rb +18 -8
- data/lib/deploy/cli/websocket_client.rb +20 -11
- data/lib/deploy/cli.rb +79 -51
- data/lib/deploy/configuration.rb +9 -2
- data/lib/deploy/errors.rb +5 -1
- data/lib/deploy/request.rb +22 -13
- data/lib/deploy/resource.rb +41 -31
- data/lib/deploy/resources/deployment.rb +9 -5
- data/lib/deploy/resources/deployment_step.rb +5 -1
- data/lib/deploy/resources/deployment_step_log.rb +9 -1
- data/lib/deploy/resources/project.rb +15 -8
- data/lib/deploy/resources/server.rb +12 -8
- data/lib/deploy/resources/server_group.rb +13 -9
- data/lib/deploy/version.rb +10 -1
- data/lib/deploy.rb +6 -0
- metadata +16 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49b0d4dee17978ab6c1930da674f450c2a39f510e7ea16c7e780071ce4d01645
|
4
|
+
data.tar.gz: 605d88bb4aab28b84f66b9924dfcce27321b1c483498fd932600f29058e77de6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2475fd2e69653132c6d19b41de8f1c3a842dca009822965ec7e2d5b81c9a57200d3aeab53cf7746a98d3c19a111f127d7749d5611e80ae5dcdb309253f073f10
|
7
|
+
data.tar.gz: d4239817c17165c41324afe6e895745e6062a55fdb6f9800850fab675636ef59038b94d5872eccc6d74f0d9bb3c611ffc394bac67c75affeadf2155dc89e23ef
|
data/bin/deployhq
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class CLI
|
5
|
+
|
3
6
|
class DeploymentProgressOutput
|
7
|
+
|
4
8
|
SERVER_TAG_COLOURS = %w[32 33 34 35 36].cycle
|
5
9
|
|
6
|
-
attr_reader :deployment
|
10
|
+
attr_reader :deployment
|
11
|
+
attr_reader :step_index
|
12
|
+
attr_reader :server_tags
|
7
13
|
|
8
14
|
def initialize(deployment)
|
9
15
|
@deployment = deployment
|
@@ -26,13 +32,14 @@ module Deploy
|
|
26
32
|
|
27
33
|
private
|
28
34
|
|
35
|
+
# rubocop:disable Metrics/AbcSize
|
29
36
|
def handle_log_entry(payload)
|
30
37
|
step = step_index[payload['step']]
|
31
38
|
server_tag = server_tags[step.server]
|
32
39
|
|
33
|
-
|
34
|
-
|
35
|
-
|
40
|
+
lines = ["\n"]
|
41
|
+
lines << server_tag
|
42
|
+
lines << payload['message']
|
36
43
|
|
37
44
|
if payload['detail']
|
38
45
|
padding_width = 0
|
@@ -40,22 +47,25 @@ module Deploy
|
|
40
47
|
padding = ' ' * padding_width
|
41
48
|
|
42
49
|
payload['detail'].split("\n").each do |detail_line|
|
43
|
-
|
50
|
+
lines << "\n#{padding}| #{detail_line}"
|
44
51
|
end
|
45
52
|
end
|
46
53
|
|
47
|
-
|
54
|
+
$stdout.print lines.join
|
48
55
|
end
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
49
57
|
|
50
58
|
def handle_status_change(payload)
|
51
59
|
if payload['status'] == 'completed'
|
52
|
-
|
60
|
+
$stdout.print "\nDeployment has finished successfully!\n"
|
53
61
|
elsif payload['status'] == 'failed'
|
54
|
-
|
62
|
+
$stdout.print "\nDeployment has failed!\n"
|
55
63
|
end
|
56
64
|
|
57
65
|
throw(:finished) if %w[completed failed].include?(payload['status'])
|
58
66
|
end
|
67
|
+
|
59
68
|
end
|
69
|
+
|
60
70
|
end
|
61
71
|
end
|
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'websocket-eventmachine-client'
|
2
4
|
require 'logger'
|
3
5
|
|
4
6
|
module Deploy
|
5
7
|
class CLI
|
8
|
+
|
6
9
|
# Manages a connection and associated subscriptions to DeployHQ's websocket
|
7
10
|
class WebsocketClient
|
11
|
+
|
8
12
|
attr_reader :subscriptions
|
9
13
|
|
10
14
|
class Subscription
|
11
|
-
|
15
|
+
|
16
|
+
attr_reader :exchange
|
17
|
+
attr_reader :routing_key
|
12
18
|
|
13
19
|
def initialize(exchange, routing_key)
|
14
20
|
@exchange = exchange
|
@@ -36,6 +42,7 @@ module Deploy
|
|
36
42
|
def subscribed!
|
37
43
|
@subscribed = true
|
38
44
|
end
|
45
|
+
|
39
46
|
end
|
40
47
|
|
41
48
|
def initialize
|
@@ -51,10 +58,10 @@ module Deploy
|
|
51
58
|
catch(:finished) do
|
52
59
|
EM.run do
|
53
60
|
connection.onopen do
|
54
|
-
logger.info
|
61
|
+
logger.info 'connected'
|
55
62
|
end
|
56
63
|
|
57
|
-
connection.onmessage do |msg,
|
64
|
+
connection.onmessage do |msg, _type|
|
58
65
|
receive(msg)
|
59
66
|
end
|
60
67
|
|
@@ -68,7 +75,7 @@ module Deploy
|
|
68
75
|
|
69
76
|
private
|
70
77
|
|
71
|
-
def dispatch(event, payload,
|
78
|
+
def dispatch(event, payload, rmq = nil)
|
72
79
|
case event
|
73
80
|
when 'Welcome'
|
74
81
|
authenticate
|
@@ -79,7 +86,7 @@ module Deploy
|
|
79
86
|
when 'Error', 'InternalError'
|
80
87
|
websocket_error(payload)
|
81
88
|
else
|
82
|
-
subscription_event(event, payload,
|
89
|
+
subscription_event(event, payload, rmq) if rmq
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
@@ -88,7 +95,7 @@ module Deploy
|
|
88
95
|
end
|
89
96
|
|
90
97
|
def request_subscriptions
|
91
|
-
subscriptions.
|
98
|
+
subscriptions.each_value do |subscription|
|
92
99
|
send('Subscribe', exchange: subscription.exchange, routing_key: subscription.routing_key)
|
93
100
|
end
|
94
101
|
end
|
@@ -96,17 +103,17 @@ module Deploy
|
|
96
103
|
def successful_subscription(payload)
|
97
104
|
key = subscription_key(payload['exchange'], payload['routing_key'])
|
98
105
|
subscription = subscriptions[key]
|
99
|
-
subscription
|
106
|
+
subscription&.subscribed!
|
100
107
|
end
|
101
108
|
|
102
109
|
def websocket_error(payload)
|
103
110
|
raise Deploy::Errors::WebsocketError, payload['error']
|
104
111
|
end
|
105
112
|
|
106
|
-
def subscription_event(event, payload,
|
107
|
-
key = subscription_key(
|
113
|
+
def subscription_event(event, payload, rmq)
|
114
|
+
key = subscription_key(rmq['e'], rmq['rk'])
|
108
115
|
subscription = subscriptions[key]
|
109
|
-
subscription
|
116
|
+
subscription&.dispatch(event, payload)
|
110
117
|
end
|
111
118
|
|
112
119
|
def receive(msg)
|
@@ -131,7 +138,7 @@ module Deploy
|
|
131
138
|
end
|
132
139
|
|
133
140
|
def logger
|
134
|
-
@logger ||= Logger.new(
|
141
|
+
@logger ||= Logger.new($stdout)
|
135
142
|
@logger.level = Logger::ERROR
|
136
143
|
@logger
|
137
144
|
end
|
@@ -139,6 +146,8 @@ module Deploy
|
|
139
146
|
def subscription_key(exchange, routing_key)
|
140
147
|
[exchange, routing_key].join('-')
|
141
148
|
end
|
149
|
+
|
142
150
|
end
|
151
|
+
|
143
152
|
end
|
144
153
|
end
|
data/lib/deploy/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
require 'highline/import'
|
3
5
|
|
@@ -5,43 +7,55 @@ require 'deploy'
|
|
5
7
|
require 'deploy/cli/websocket_client'
|
6
8
|
require 'deploy/cli/deployment_progress_output'
|
7
9
|
|
8
|
-
|
10
|
+
OptionsStruct = Struct.new(:config_file, :project, :config_files_deployment)
|
9
11
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
13
|
+
# rubocop:disable Metrics/AbcSize
|
14
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
16
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
10
17
|
module Deploy
|
11
18
|
class CLI
|
19
|
+
|
12
20
|
## Constants for formatting output
|
13
|
-
PROTOCOL_NAME = {:
|
21
|
+
PROTOCOL_NAME = { ssh: 'SSH/SFTP', ftp: 'FTP', s3: 'Amazon S3', rackspace: 'Rackspace CloudFiles' }.freeze
|
14
22
|
|
15
23
|
class << self
|
24
|
+
|
16
25
|
def invoke(args)
|
17
|
-
@options =
|
26
|
+
@options = OptionsStruct.new
|
18
27
|
|
19
|
-
parser = OptionParser.new do |opts|
|
20
|
-
opts.banner =
|
21
|
-
opts.separator
|
22
|
-
opts.separator
|
28
|
+
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
29
|
+
opts.banner = 'Usage: deployhq [options] command'
|
30
|
+
opts.separator ''
|
31
|
+
opts.separator 'Commands:'
|
23
32
|
opts.separator "deploy\t\t Start a new deployment"
|
24
33
|
opts.separator "servers\t\t List configured servers and server groups"
|
25
34
|
opts.separator "configure\t\t Create a new configuration file for this tool"
|
26
|
-
opts.separator
|
27
|
-
opts.separator
|
35
|
+
opts.separator ''
|
36
|
+
opts.separator 'Common Options:'
|
28
37
|
|
29
38
|
@options.config_file = './Deployfile'
|
30
|
-
opts.on(
|
39
|
+
opts.on('-c', '--config path', 'Configuration file path') do |config_file_path|
|
31
40
|
@options.config_file = config_file_path
|
32
41
|
end
|
33
42
|
|
34
|
-
opts.on(
|
35
|
-
|
43
|
+
opts.on('-p', '--project project',
|
44
|
+
'Project to operate on (default is read from project: in config file)') do |project_permalink|
|
36
45
|
@options.project = project_permalink
|
37
46
|
end
|
38
47
|
|
39
|
-
|
48
|
+
@options.config_files_deployment = false
|
49
|
+
opts.on('--config-files', 'Config files deployment') do |_config_files_deployment|
|
50
|
+
@options.config_files_deployment = true
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on_tail('-v', '--version', 'Shows Version') do
|
40
54
|
puts Deploy::VERSION
|
41
55
|
exit
|
42
56
|
end
|
43
57
|
|
44
|
-
opts.on_tail(
|
58
|
+
opts.on_tail('-h', '--help', 'Displays Help') do
|
45
59
|
puts opts
|
46
60
|
exit
|
47
61
|
end
|
@@ -51,7 +65,7 @@ module Deploy
|
|
51
65
|
parser.parse!(args)
|
52
66
|
command = args.pop
|
53
67
|
rescue OptionParser::InvalidOption
|
54
|
-
|
68
|
+
warn parser
|
55
69
|
exit 1
|
56
70
|
end
|
57
71
|
|
@@ -59,13 +73,13 @@ module Deploy
|
|
59
73
|
begin
|
60
74
|
Deploy.configuration_file = @options.config_file
|
61
75
|
rescue Errno::ENOENT
|
62
|
-
|
76
|
+
warn "Couldn't find configuration file at #{@options.config_file.inspect}"
|
63
77
|
exit 1
|
64
78
|
end
|
65
79
|
|
66
80
|
project_permalink = @options.project || Deploy.configuration.project
|
67
81
|
if project_permalink.nil?
|
68
|
-
|
82
|
+
warn 'Project must be specified in config file or as --project argument'
|
69
83
|
exit 1
|
70
84
|
end
|
71
85
|
|
@@ -80,25 +94,25 @@ module Deploy
|
|
80
94
|
when 'configure'
|
81
95
|
configure
|
82
96
|
else
|
83
|
-
|
97
|
+
warn parser
|
84
98
|
end
|
85
99
|
end
|
86
100
|
|
87
101
|
def server_list
|
88
102
|
@server_groups ||= @project.server_groups
|
89
|
-
if @server_groups.count
|
103
|
+
if @server_groups.count.positive?
|
90
104
|
@server_groups.each do |group|
|
91
|
-
puts "Group: #{group.name}"
|
92
|
-
puts group.servers.map {|server| format_server(server) }.join("\n\n")
|
105
|
+
puts "Group: #{group.name}"
|
106
|
+
puts group.servers.map { |server| format_server(server) }.join("\n\n")
|
93
107
|
end
|
94
108
|
end
|
95
109
|
|
96
110
|
@ungrouped_servers ||= @project.servers
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
111
|
+
return unless @ungrouped_servers.count.positive?
|
112
|
+
|
113
|
+
puts "\n" if @server_groups.count.positive?
|
114
|
+
puts 'Ungrouped Servers'
|
115
|
+
puts @ungrouped_servers.map { |server| format_server(server) }.join("\n\n")
|
102
116
|
end
|
103
117
|
|
104
118
|
def deploy
|
@@ -108,34 +122,40 @@ module Deploy
|
|
108
122
|
parent = nil
|
109
123
|
while parent.nil?
|
110
124
|
parent = choose do |menu|
|
111
|
-
menu.prompt =
|
125
|
+
menu.prompt = 'Please choose a server or group to deploy to:'
|
112
126
|
|
113
127
|
menu.choices(*(@ungrouped_servers + @server_groups))
|
114
|
-
menu.choice(
|
128
|
+
menu.choice('List Server Details') do
|
115
129
|
server_list
|
116
130
|
nil
|
117
131
|
end
|
118
132
|
end
|
119
133
|
end
|
120
134
|
|
121
|
-
|
122
|
-
|
135
|
+
if @options.config_files_deployment
|
136
|
+
$stdout.print "\nStarting config files deployment\n"
|
137
|
+
deployment = @project.config_files_deployment(parent.identifier)
|
138
|
+
else
|
139
|
+
$stdout.print "\nStarting deployment\n"
|
140
|
+
latest_revision = @project.latest_revision(parent.preferred_branch)
|
141
|
+
deployment = @project.deploy(parent.identifier, parent.last_revision, latest_revision)
|
142
|
+
end
|
123
143
|
|
124
|
-
|
144
|
+
$stdout.print 'Waiting for an available deployment slot...'
|
125
145
|
DeploymentProgressOutput.new(deployment).monitor
|
126
146
|
end
|
127
147
|
|
128
148
|
def configure
|
129
149
|
configuration = {
|
130
|
-
account: ask_config_question(
|
131
|
-
|
132
|
-
username: ask_config_question(
|
133
|
-
api_key: ask_config_question(
|
134
|
-
project: ask_config_question(
|
150
|
+
account: ask_config_question('Account Domain (e.g. https://atech.deployhq.com)',
|
151
|
+
/\Ahttps?:\/\/[a-z0-9.-]+.deployhq.com\z/),
|
152
|
+
username: ask_config_question('Username or e-mail address'),
|
153
|
+
api_key: ask_config_question('API key (You can find this in Settings -> Security)'),
|
154
|
+
project: ask_config_question('Default project to use (please use permalink from web URL)')
|
135
155
|
}
|
136
156
|
|
137
157
|
confirmation = true
|
138
|
-
if File.
|
158
|
+
if File.exist?(@options.config_file)
|
139
159
|
confirmation = agree("File already exists at #{@options.config_file}. Overwrite? ")
|
140
160
|
end
|
141
161
|
|
@@ -150,7 +170,7 @@ module Deploy
|
|
150
170
|
question_text = "#{question_text}: "
|
151
171
|
ask(question_text) do |q|
|
152
172
|
q.whitespace = :remove
|
153
|
-
q.responses[:not_valid] =
|
173
|
+
q.responses[:not_valid] = 'That answer is not valid'
|
154
174
|
q.responses[:ask_on_error] = :question
|
155
175
|
q.validate = valid_format
|
156
176
|
end
|
@@ -161,30 +181,38 @@ module Deploy
|
|
161
181
|
## Data formatters
|
162
182
|
def format_server(server)
|
163
183
|
server_params = {
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
184
|
+
'Name' => server.name,
|
185
|
+
'Type' => PROTOCOL_NAME[server.protocol_type.to_sym],
|
186
|
+
'Path' => server.server_path,
|
187
|
+
'Branch' => server.preferred_branch,
|
188
|
+
'Current Revision' => server.last_revision
|
169
189
|
}
|
170
|
-
server_params[
|
171
|
-
server_params[
|
172
|
-
server_params[
|
173
|
-
server_params[
|
190
|
+
server_params['Hostname'] = [server.hostname, server.port].join(':') if server.hostname
|
191
|
+
server_params['Bucket'] = server.bucket_name if server.bucket_name
|
192
|
+
server_params['Region'] = server.region if server.region
|
193
|
+
server_params['Container'] = server.container_name if server.container_name
|
174
194
|
|
175
|
-
|
195
|
+
[].tap do |a|
|
176
196
|
a << format_kv_pair(server_params)
|
177
197
|
end.join("\n")
|
178
198
|
end
|
179
199
|
|
200
|
+
# rubocop:disable Lint/FormatParameterMismatch
|
180
201
|
def format_kv_pair(hash)
|
181
202
|
longest_key = hash.keys.map(&:length).max + 2
|
182
|
-
hash.each_with_index.map do |(k,v),
|
183
|
-
str =
|
184
|
-
|
203
|
+
hash.each_with_index.map do |(k, v), _i|
|
204
|
+
str = format("%#{longest_key}s : %s", k, v)
|
205
|
+
str
|
185
206
|
end.join("\n")
|
186
207
|
end
|
208
|
+
# rubocop:enable Lint/FormatParameterMismatch
|
187
209
|
|
188
210
|
end
|
211
|
+
|
189
212
|
end
|
190
213
|
end
|
214
|
+
# rubocop:enable Metrics/ClassLength
|
215
|
+
# rubocop:enable Metrics/AbcSize
|
216
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
217
|
+
# rubocop:enable Metrics/MethodLength
|
218
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
data/lib/deploy/configuration.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class Configuration
|
3
|
-
|
5
|
+
|
6
|
+
attr_accessor :account
|
7
|
+
attr_accessor :username
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :project
|
4
10
|
attr_writer :websocket_hostname
|
5
11
|
|
6
12
|
def websocket_hostname
|
@@ -11,7 +17,7 @@ module Deploy
|
|
11
17
|
file_contents = File.read(path)
|
12
18
|
parsed_contents = JSON.parse(file_contents)
|
13
19
|
|
14
|
-
|
20
|
+
new.tap do |config|
|
15
21
|
config.account = parsed_contents['account']
|
16
22
|
config.username = parsed_contents['username']
|
17
23
|
config.api_key = parsed_contents['api_key']
|
@@ -19,5 +25,6 @@ module Deploy
|
|
19
25
|
config.websocket_hostname = parsed_contents['websocket_hostname']
|
20
26
|
end
|
21
27
|
end
|
28
|
+
|
22
29
|
end
|
23
30
|
end
|
data/lib/deploy/errors.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
|
3
5
|
## Base level error which all other deploy errors will inherit from. It may also be
|
@@ -13,10 +15,12 @@ module Deploy
|
|
13
15
|
## Access was denied to the remote service
|
14
16
|
class AccessDenied < Error; end
|
15
17
|
|
16
|
-
## A communication error
|
18
|
+
## A communication error occurred while talking to the Deploy API
|
17
19
|
class CommunicationError < Error; end
|
18
20
|
|
19
21
|
# Raised from the websocket client when we receive an error event
|
20
22
|
class WebsocketError < Error; end
|
23
|
+
|
21
24
|
end
|
25
|
+
|
22
26
|
end
|
data/lib/deploy/request.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# rubocop:disable Metrics/AbcSize
|
6
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
1
8
|
module Deploy
|
2
9
|
class Request
|
3
10
|
|
4
|
-
attr_reader :path
|
11
|
+
attr_reader :path
|
12
|
+
attr_reader :method
|
5
13
|
attr_accessor :data
|
6
14
|
|
7
15
|
def initialize(path, method = :get)
|
@@ -23,30 +31,28 @@ module Deploy
|
|
23
31
|
uri = URI.parse([Deploy.configuration.account, @path].join('/'))
|
24
32
|
http_request = http_class.new(uri.request_uri)
|
25
33
|
http_request.basic_auth(Deploy.configuration.username, Deploy.configuration.api_key)
|
26
|
-
http_request[
|
27
|
-
http_request[
|
34
|
+
http_request['Accept'] = 'application/json'
|
35
|
+
http_request['Content-Type'] = 'application/json'
|
28
36
|
|
29
37
|
http = Net::HTTP.new(uri.host, uri.port)
|
30
|
-
if uri.scheme == 'https'
|
31
|
-
http.use_ssl = true
|
32
|
-
end
|
38
|
+
http.use_ssl = true if uri.scheme == 'https'
|
33
39
|
|
34
40
|
data = self.data.to_json if self.data.is_a?(Hash) && self.data.respond_to?(:to_json)
|
35
41
|
http_result = http.request(http_request, data)
|
36
42
|
@output = http_result.body
|
37
|
-
|
43
|
+
case http_result
|
38
44
|
when Net::HTTPSuccess
|
39
|
-
true
|
45
|
+
@success = true
|
40
46
|
when Net::HTTPServiceUnavailable
|
41
|
-
raise Deploy::Errors::ServiceUnavailable
|
47
|
+
@success = raise Deploy::Errors::ServiceUnavailable
|
42
48
|
when Net::HTTPForbidden, Net::HTTPUnauthorized
|
43
|
-
raise Deploy::Errors::AccessDenied, "Access Denied for '#{Deploy.configuration.username}'"
|
49
|
+
@success = raise Deploy::Errors::AccessDenied, "Access Denied for '#{Deploy.configuration.username}'"
|
44
50
|
when Net::HTTPNotFound
|
45
|
-
raise Deploy::Errors::CommunicationError, "Not Found at #{uri
|
51
|
+
@success = raise Deploy::Errors::CommunicationError, "Not Found at #{uri}"
|
46
52
|
when Net::HTTPClientError
|
47
|
-
false
|
53
|
+
@success = false
|
48
54
|
else
|
49
|
-
raise Deploy::Errors::CommunicationError, http_result.body
|
55
|
+
@success = raise Deploy::Errors::CommunicationError, http_result.body
|
50
56
|
end
|
51
57
|
self
|
52
58
|
end
|
@@ -65,3 +71,6 @@ module Deploy
|
|
65
71
|
|
66
72
|
end
|
67
73
|
end
|
74
|
+
# rubocop:enable Metrics/AbcSize
|
75
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
76
|
+
# rubocop:enable Metrics/MethodLength
|
data/lib/deploy/resource.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class Resource
|
3
5
|
|
@@ -9,14 +11,18 @@ module Deploy
|
|
9
11
|
def method_missing(method, *params)
|
10
12
|
set = method.to_s.include?('=')
|
11
13
|
key = method.to_s.sub('=', '')
|
12
|
-
self.attributes =
|
14
|
+
self.attributes = ({}) unless attributes.is_a?(Hash)
|
13
15
|
if set
|
14
|
-
|
16
|
+
attributes[key] = params.first
|
15
17
|
else
|
16
|
-
|
18
|
+
attributes[key]
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
22
|
+
def respond_to_missing?(method_name, include_private = false)
|
23
|
+
method_name.to_s.start_with?('user_') || super
|
24
|
+
end
|
25
|
+
|
20
26
|
class << self
|
21
27
|
|
22
28
|
## Find a record or set of records. Passing :all will return all records and passing an integer
|
@@ -30,11 +36,13 @@ module Deploy
|
|
30
36
|
|
31
37
|
## Find all objects and return an array of objects with the attributes set.
|
32
38
|
def find_all(params)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
request = Request.new(collection_path(params))
|
40
|
+
output = request.make.output
|
41
|
+
output = JSON.parse(output)
|
42
|
+
|
43
|
+
output = output['records'] if output.is_a?(Hash) && output['records'] && output['pagination']
|
37
44
|
return [] unless output.is_a?(Array)
|
45
|
+
|
38
46
|
output.map do |o|
|
39
47
|
create_object(o, params)
|
40
48
|
end
|
@@ -42,12 +50,13 @@ module Deploy
|
|
42
50
|
|
43
51
|
## Find a single object and return an object for it.
|
44
52
|
def find_single(id, params = {})
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
53
|
+
request = Request.new(member_path(id, params))
|
54
|
+
output = request.make.output
|
55
|
+
output = JSON.parse(output)
|
56
|
+
|
57
|
+
raise Deploy::Errors::NotFound, 'Record not found' unless output.is_a?(Hash)
|
58
|
+
|
59
|
+
create_object(output, params)
|
51
60
|
end
|
52
61
|
|
53
62
|
## Post to the specified object on the collection path
|
@@ -58,18 +67,18 @@ module Deploy
|
|
58
67
|
## Return the collection path for this model. Very lazy pluralizion here
|
59
68
|
## at the moment, nothing in Deploy needs to be pluralized with anything
|
60
69
|
## other than just adding an 's'.
|
61
|
-
def collection_path(
|
62
|
-
class_name.downcase
|
70
|
+
def collection_path(_params = {})
|
71
|
+
"#{class_name.downcase}s"
|
63
72
|
end
|
64
73
|
|
65
74
|
## Return the member path for the passed ID & attributes
|
66
|
-
def member_path(id,
|
75
|
+
def member_path(id, _params = {})
|
67
76
|
[collection_path, id].join('/')
|
68
77
|
end
|
69
78
|
|
70
79
|
## Return the deploy class name
|
71
80
|
def class_name
|
72
|
-
|
81
|
+
name.to_s.split('::').last.downcase
|
73
82
|
end
|
74
83
|
|
75
84
|
private
|
@@ -77,20 +86,21 @@ module Deploy
|
|
77
86
|
## Create a new object with the specified attributes and getting and ID.
|
78
87
|
## Returns the newly created object
|
79
88
|
def create_object(attributes, objects = [])
|
80
|
-
o =
|
89
|
+
o = new
|
81
90
|
o.attributes = attributes
|
82
91
|
o.id = attributes['id']
|
83
|
-
|
92
|
+
objects.select { |_k, v| v.is_a?(Deploy::Resource) }.each do |key, object|
|
84
93
|
o.attributes[key.to_s] = object
|
85
94
|
end
|
86
95
|
o
|
87
96
|
end
|
97
|
+
|
88
98
|
end
|
89
99
|
|
90
100
|
## Run a post on the member path. Returns the ouput from the post, false if a conflict or raises
|
91
101
|
## a Deploy::Error. Optionally, pass a second 'data' parameter to send data to the post action.
|
92
102
|
def post(action, data = nil)
|
93
|
-
path = self.class.member_path(
|
103
|
+
path = "#{self.class.member_path(id, default_params)}/#{action}"
|
94
104
|
request = Request.new(path, :post)
|
95
105
|
request.data = data
|
96
106
|
request.make
|
@@ -99,20 +109,21 @@ module Deploy
|
|
99
109
|
## Delete this record from the remote service. Returns true or false depending on the success
|
100
110
|
## status of the destruction.
|
101
111
|
def destroy
|
102
|
-
Request.new(self.class.member_path(
|
112
|
+
Request.new(self.class.member_path(id, default_params), :delete).make.success?
|
103
113
|
end
|
104
114
|
|
105
115
|
def new_record?
|
106
|
-
|
116
|
+
id.nil?
|
107
117
|
end
|
108
118
|
|
109
119
|
def save
|
110
120
|
new_record? ? create : update
|
111
121
|
end
|
112
122
|
|
123
|
+
# rubocop:disable Metrics/AbcSize
|
113
124
|
def create
|
114
125
|
request = Request.new(self.class.collection_path(default_params), :post)
|
115
|
-
request.data = {self.class.class_name.downcase.to_sym => attributes_to_post}
|
126
|
+
request.data = { self.class.class_name.downcase.to_sym => attributes_to_post }
|
116
127
|
if request.make && request.success?
|
117
128
|
new_record = JSON.parse(request.output)
|
118
129
|
self.attributes = new_record
|
@@ -123,13 +134,14 @@ module Deploy
|
|
123
134
|
false
|
124
135
|
end
|
125
136
|
end
|
137
|
+
# rubocop:enable Metrics/AbcSize
|
126
138
|
|
127
139
|
## Push the updated attributes to the remote. Returns true if the record was saved successfully
|
128
140
|
## other false if not. If not saved successfully, the errors hash will be updated with an array
|
129
141
|
## of all errors with the submission.
|
130
142
|
def update
|
131
|
-
request = Request.new(self.class.member_path(
|
132
|
-
request.data = {self.class.class_name.downcase.to_sym => attributes_to_post}
|
143
|
+
request = Request.new(self.class.member_path(id, default_params), :put)
|
144
|
+
request.data = { self.class.class_name.downcase.to_sym => attributes_to_post }
|
133
145
|
if request.make && request.success?
|
134
146
|
true
|
135
147
|
else
|
@@ -142,23 +154,21 @@ module Deploy
|
|
142
154
|
|
143
155
|
## Populate the errors hash from the given raw JSON output
|
144
156
|
def populate_errors(json)
|
145
|
-
self.errors =
|
146
|
-
JSON.parse(json).
|
157
|
+
self.errors = ({})
|
158
|
+
JSON.parse(json).each_with_object(errors) do |e, r|
|
147
159
|
r[e.first] = e.last
|
148
|
-
r
|
149
160
|
end
|
150
161
|
end
|
151
162
|
|
152
163
|
## An array of params which should always be sent with this instances requests
|
153
164
|
def default_params
|
154
|
-
|
165
|
+
{}
|
155
166
|
end
|
156
167
|
|
157
168
|
## Attributes which can be passed for update & creation
|
158
169
|
def attributes_to_post
|
159
|
-
|
170
|
+
attributes.each_with_object({}) do |(key, value), r|
|
160
171
|
r[key] = value if value.is_a?(String) || value.is_a?(Integer)
|
161
|
-
r
|
162
172
|
end
|
163
173
|
end
|
164
174
|
|
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class Deployment < Resource
|
5
|
+
|
3
6
|
class << self
|
7
|
+
|
4
8
|
def collection_path(params = {})
|
5
9
|
"projects/#{params[:project].permalink}/deployments"
|
6
10
|
end
|
@@ -8,17 +12,16 @@ module Deploy
|
|
8
12
|
def member_path(id, params = {})
|
9
13
|
"projects/#{params[:project].permalink}/deployments/#{id}"
|
10
14
|
end
|
15
|
+
|
11
16
|
end
|
12
17
|
|
13
18
|
def default_params
|
14
|
-
{:project
|
19
|
+
{ project: project }
|
15
20
|
end
|
16
21
|
|
17
22
|
def project
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
self.attributes['project']
|
23
|
+
attributes['project'] = Project.send(:create_object, attributes['project']) if attributes['project'].is_a?(Hash)
|
24
|
+
attributes['project']
|
22
25
|
end
|
23
26
|
|
24
27
|
def servers
|
@@ -47,5 +50,6 @@ module Deploy
|
|
47
50
|
[]
|
48
51
|
end
|
49
52
|
end
|
53
|
+
|
50
54
|
end
|
51
55
|
end
|
@@ -1,12 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class DeploymentStep < Resource
|
5
|
+
|
3
6
|
def default_params
|
4
|
-
{ deployment:
|
7
|
+
{ deployment: deployment, project: deployment.project }
|
5
8
|
end
|
6
9
|
|
7
10
|
def logs(params = {})
|
8
11
|
params = default_params.merge(step: self).merge(params)
|
9
12
|
DeploymentStepLog.find(:all, params)
|
10
13
|
end
|
14
|
+
|
11
15
|
end
|
12
16
|
end
|
@@ -1,7 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class DeploymentStepLog < Resource
|
5
|
+
|
3
6
|
def self.collection_path(params = {})
|
4
|
-
|
7
|
+
permalink = params[:project].permalink
|
8
|
+
project_identifier = params[:project].identifier
|
9
|
+
step_identifier = params[:step].identifier
|
10
|
+
|
11
|
+
"projects/#{permalink}/deployments/#{project_identifier}/steps/#{step_identifier}/logs"
|
5
12
|
end
|
13
|
+
|
6
14
|
end
|
7
15
|
end
|
@@ -1,31 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class Project < Resource
|
3
5
|
|
4
6
|
## Return all deployments for this project
|
5
7
|
def deployments
|
6
|
-
Deployment.find(:all, :
|
8
|
+
Deployment.find(:all, project: self)
|
7
9
|
end
|
8
10
|
|
9
11
|
## Return a deployment
|
10
12
|
def deployment(identifier)
|
11
|
-
Deployment.find(identifier, :
|
13
|
+
Deployment.find(identifier, project: self)
|
12
14
|
end
|
13
15
|
|
14
16
|
def latest_revision(branch = '')
|
15
17
|
branch ||= 'master'
|
16
|
-
req = Request.new(self.class.member_path(
|
18
|
+
req = Request.new(self.class.member_path(permalink) + "/repository/latest_revision?branch=#{branch}").make
|
17
19
|
parsed = JSON.parse(req.output)
|
18
20
|
parsed['ref']
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
def deploy(server, start_revision, end_revision)
|
23
|
+
# Create a deployment in this project (and queue it to run)
|
24
|
+
def deploy(server, start_revision, end_revision, config_files_only: false)
|
23
25
|
run_deployment(server, start_revision, end_revision) do |d|
|
24
26
|
d.mode = 'queue'
|
27
|
+
d.config_files_deployment = '1' if config_files_only
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
28
|
-
|
31
|
+
def config_files_deployment(server)
|
32
|
+
deploy(server, nil, nil, config_files_only: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a deployment preview
|
29
36
|
def preview(server, start_revision, end_revision)
|
30
37
|
run_deployment(server, start_revision, end_revision) do |d|
|
31
38
|
d.mode = 'preview'
|
@@ -34,11 +41,11 @@ module Deploy
|
|
34
41
|
|
35
42
|
## Return all servers for this project
|
36
43
|
def servers
|
37
|
-
Server.find(:all, :
|
44
|
+
Server.find(:all, project: self)
|
38
45
|
end
|
39
46
|
|
40
47
|
def server_groups
|
41
|
-
ServerGroup.find(:all, :
|
48
|
+
ServerGroup.find(:all, project: self)
|
42
49
|
end
|
43
50
|
|
44
51
|
private
|
@@ -1,28 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class Server < Resource
|
3
5
|
|
4
6
|
class << self
|
7
|
+
|
5
8
|
def collection_path(params = {})
|
6
9
|
"projects/#{params[:project].permalink}/servers"
|
7
10
|
end
|
8
11
|
|
9
|
-
def member_path(
|
12
|
+
def member_path(_id, params = {})
|
10
13
|
"projects/#{params[:project].permalink}/servers/#{identifier}"
|
11
14
|
end
|
15
|
+
|
12
16
|
end
|
13
17
|
|
14
18
|
def default_params
|
15
|
-
{:project
|
19
|
+
{ project: project }
|
16
20
|
end
|
17
21
|
|
18
22
|
def to_s
|
19
|
-
|
20
|
-
a <<
|
21
|
-
a << "(branch: #{
|
22
|
-
if
|
23
|
-
a << "(currently: #{
|
23
|
+
[].tap do |a|
|
24
|
+
a << name
|
25
|
+
a << "(branch: #{preferred_branch})" if preferred_branch
|
26
|
+
if last_revision
|
27
|
+
a << "(currently: #{last_revision})"
|
24
28
|
else
|
25
|
-
a <<
|
29
|
+
a << '(currently undeployed)'
|
26
30
|
end
|
27
31
|
end.join(' ')
|
28
32
|
end
|
@@ -1,32 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
4
|
class ServerGroup < Resource
|
3
5
|
|
4
6
|
class << self
|
7
|
+
|
5
8
|
def collection_path(params = {})
|
6
9
|
"projects/#{params[:project].permalink}/server_groups"
|
7
10
|
end
|
8
11
|
|
9
|
-
def member_path(
|
12
|
+
def member_path(_id, params = {})
|
10
13
|
"projects/#{params[:project].permalink}/server_groups/#{identifier}"
|
11
14
|
end
|
15
|
+
|
12
16
|
end
|
13
17
|
|
14
18
|
def default_params
|
15
|
-
{:project
|
19
|
+
{ project: project }
|
16
20
|
end
|
17
21
|
|
18
22
|
def servers
|
19
|
-
@servers ||=
|
23
|
+
@servers ||= attributes['servers'].map { |server_attr| Deploy::Server.send(:create_object, server_attr) }
|
20
24
|
end
|
21
25
|
|
22
26
|
def to_s
|
23
|
-
|
24
|
-
a <<
|
25
|
-
a << "(branch: #{
|
26
|
-
if
|
27
|
-
a << "(currently: #{
|
27
|
+
[].tap do |a|
|
28
|
+
a << name
|
29
|
+
a << "(branch: #{preferred_branch})" if preferred_branch
|
30
|
+
if last_revision
|
31
|
+
a << "(currently: #{last_revision})"
|
28
32
|
else
|
29
|
-
a <<
|
33
|
+
a << '(currently undeployed)'
|
30
34
|
end
|
31
35
|
end.join(' ')
|
32
36
|
end
|
data/lib/deploy/version.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Deploy
|
2
|
-
|
4
|
+
|
5
|
+
VERSION_FILE_ROOT = File.expand_path('../../VERSION', __dir__)
|
6
|
+
if File.file?(VERSION_FILE_ROOT)
|
7
|
+
VERSION = File.read(VERSION_FILE_ROOT).strip.sub(/\Av/, '')
|
8
|
+
else
|
9
|
+
VERSION = '0.0.0.dev'
|
10
|
+
end
|
11
|
+
|
3
12
|
end
|
data/lib/deploy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'bundler'
|
3
5
|
|
@@ -25,7 +27,9 @@ require 'deploy/resources/server_group'
|
|
25
27
|
require 'deploy/version'
|
26
28
|
|
27
29
|
module Deploy
|
30
|
+
|
28
31
|
class << self
|
32
|
+
|
29
33
|
def configure
|
30
34
|
@configuration ||= Configuration.new
|
31
35
|
yield @configuration if block_given?
|
@@ -39,5 +43,7 @@ module Deploy
|
|
39
43
|
def configuration_file=(file_location)
|
40
44
|
@configuration = Configuration.from_file(file_location)
|
41
45
|
end
|
46
|
+
|
42
47
|
end
|
48
|
+
|
43
49
|
end
|
metadata
CHANGED
@@ -1,43 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deployhq
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
8
|
-
autorequire:
|
7
|
+
- Adam Cooke
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-08 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
13
|
+
name: highline
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
16
|
- - "~>"
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1
|
18
|
+
version: '2.1'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - "~>"
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1
|
25
|
+
version: '2.1'
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
27
|
+
name: json
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
30
29
|
requirements:
|
31
30
|
- - "~>"
|
32
31
|
- !ruby/object:Gem::Version
|
33
|
-
version: '2.
|
32
|
+
version: '2.6'
|
34
33
|
type: :runtime
|
35
34
|
prerelease: false
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
37
36
|
requirements:
|
38
37
|
- - "~>"
|
39
38
|
- !ruby/object:Gem::Version
|
40
|
-
version: '2.
|
39
|
+
version: '2.6'
|
41
40
|
- !ruby/object:Gem::Dependency
|
42
41
|
name: websocket-eventmachine-client
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,10 +51,10 @@ dependencies:
|
|
52
51
|
- - "~>"
|
53
52
|
- !ruby/object:Gem::Version
|
54
53
|
version: '1.2'
|
55
|
-
description:
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
description: API and CLI client for the DeployHQ deployment platform. Provides the
|
55
|
+
deployhq executable.
|
56
|
+
email:
|
57
|
+
- adam@k.io
|
59
58
|
executables:
|
60
59
|
- deployhq
|
61
60
|
extensions: []
|
@@ -77,11 +76,10 @@ files:
|
|
77
76
|
- lib/deploy/resources/server.rb
|
78
77
|
- lib/deploy/resources/server_group.rb
|
79
78
|
- lib/deploy/version.rb
|
80
|
-
homepage: https://
|
79
|
+
homepage: https://github.com/krystal/deployhq-lib
|
81
80
|
licenses:
|
82
81
|
- MIT
|
83
82
|
metadata: {}
|
84
|
-
post_install_message:
|
85
83
|
rdoc_options: []
|
86
84
|
require_paths:
|
87
85
|
- lib
|
@@ -89,15 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
87
|
requirements:
|
90
88
|
- - ">="
|
91
89
|
- !ruby/object:Gem::Version
|
92
|
-
version: '
|
90
|
+
version: '2.7'
|
93
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
92
|
requirements:
|
95
93
|
- - ">="
|
96
94
|
- !ruby/object:Gem::Version
|
97
95
|
version: '0'
|
98
96
|
requirements: []
|
99
|
-
rubygems_version: 3.
|
100
|
-
signing_key:
|
97
|
+
rubygems_version: 3.6.2
|
101
98
|
specification_version: 4
|
102
99
|
summary: API and CLI client for the DeployHQ
|
103
100
|
test_files: []
|