bard 1.4.10 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 318e587da8f359d8637e0bc1ad4c2c8cdb40fae0a8abd450e7d294a6d0428c82
4
- data.tar.gz: b35fb1f00685c2549c5be7795be76c6036ac233f93ed0e2db0ea1e358167143f
3
+ metadata.gz: d0f636850eb0b2f55f75b125dfef31c7bea2bfc0d9927fbb9714431c32694369
4
+ data.tar.gz: 66c491d452f1eda598647bfced569674d367d79289d8203b1fefc145e524c6df
5
5
  SHA512:
6
- metadata.gz: 77dd9f076659596e8472012758b2fe8c14ed2345ecfdc66b1d6ce726bc698c427c8c76adac35204f09e31d357755579c2a3d7644fdd79ece28175bbcf7e52e07
7
- data.tar.gz: a1d4e0abd8bb223b7987d8961ea3b3de3a68e4113f42c56cd48024c8ecc57dc37104f8340b78168608e53dfa44589fc17305b46f7668db4af7b1058a678e2a16
6
+ metadata.gz: c87fd3c0723f0be35ca017d74348e7c3e711aa36a7b0ad1042ca9b1f09c0a7f675499eabc963a96d7d474693dc96adc8e887e0e2e1067efd291f379d801c9b87
7
+ data.tar.gz: 2c0a7de1674bc3db706ea4990549393c0a4fe8cc289f1a78e8171650e2343b9140158bb2c07796787e1206df40af1c0ac679118436edcaf0c7cb8e9a03f9256b
@@ -1,23 +1,10 @@
1
1
  require "time"
2
2
  require "bard/github"
3
+ require "bard/ci/runner"
3
4
 
4
5
  module Bard
5
6
  class CI
6
- class GithubActions < Struct.new(:project_name, :branch, :sha)
7
- def run
8
- last_time_elapsed = api.last_successful_run&.time_elapsed
9
- @run = api.create_run!(branch)
10
-
11
- start_time = Time.new.to_i
12
- while @run.building?
13
- elapsed_time = Time.new.to_i - start_time
14
- yield elapsed_time, last_time_elapsed
15
- sleep(2)
16
- @run = api.find_run(@run.id)
17
- end
18
-
19
- @run.success?
20
- end
7
+ class GithubActions < Runner
21
8
 
22
9
  def exists?
23
10
  true
@@ -40,6 +27,50 @@ module Bard
40
27
  end
41
28
  end
42
29
 
30
+ protected
31
+
32
+ def start
33
+ @run = api.create_run!(branch)
34
+ end
35
+
36
+ def get_last_time_elapsed
37
+ api.last_successful_run&.time_elapsed
38
+ end
39
+
40
+ def building?
41
+ @run.building?
42
+ end
43
+
44
+ def success?
45
+ @run.success?
46
+ end
47
+
48
+ def get_state_data
49
+ {
50
+ "project_name" => project_name,
51
+ "branch" => branch,
52
+ "run_id" => @run.id,
53
+ "start_time" => @start_time,
54
+ "last_time_elapsed" => @last_time_elapsed
55
+ }
56
+ end
57
+
58
+ def restore_state(data)
59
+ @run = api.find_run(data["run_id"])
60
+ @start_time = data["start_time"]
61
+ @last_time_elapsed = data["last_time_elapsed"]
62
+ end
63
+
64
+ def poll_until_complete
65
+ while @run.building?
66
+ elapsed_time = Time.new.to_i - @start_time
67
+ yield elapsed_time, @last_time_elapsed
68
+ save_state
69
+ sleep(2)
70
+ @run = api.find_run(@run.id)
71
+ end
72
+ end
73
+
43
74
  private
44
75
 
45
76
  def api
@@ -1,22 +1,9 @@
1
1
  require "json"
2
+ require "bard/ci/runner"
2
3
 
3
4
  module Bard
4
5
  class CI
5
- class Jenkins < Struct.new(:project_name, :branch, :sha)
6
- def run
7
- last_time_elapsed = get_last_time_elapsed
8
- start
9
- sleep(2) until started?
10
-
11
- start_time = Time.new.to_i
12
- while building?
13
- elapsed_time = Time.new.to_i - start_time
14
- yield elapsed_time, last_time_elapsed
15
- sleep(2)
16
- end
17
-
18
- success?
19
- end
6
+ class Jenkins < Runner
20
7
 
21
8
  def exists?
22
9
  `curl -s -I #{ci_host}/` =~ /\b200 OK\b/
@@ -29,12 +16,61 @@ module Bard
29
16
 
30
17
  attr_accessor :last_response
31
18
 
19
+ protected
20
+
21
+ def wait_until_started
22
+ sleep(2) until started?
23
+ end
24
+
25
+ def start
26
+ command = "curl -s -I -X POST -L '#{ci_host}/buildWithParameters?GIT_REF=#{sha}'"
27
+ output = `#{command}`
28
+ @queueId = output[%r{Location: .+/queue/item/(\d+)/}, 1].to_i
29
+ end
30
+
31
+ def building?
32
+ retry_with_backoff do
33
+ self.last_response = `curl -s #{ci_host}/#{job_id}/api/json?tree=building,result`
34
+ raise "Blank response from CI" if last_response.blank?
35
+ end
36
+ last_response.include? '"building":true'
37
+ end
38
+
39
+ def success?
40
+ last_response.include? '"result":"SUCCESS"'
41
+ end
42
+
43
+ def get_state_data
44
+ {
45
+ "project_name" => project_name,
46
+ "branch" => branch,
47
+ "queue_id" => @queueId,
48
+ "job_id" => @job_id,
49
+ "start_time" => @start_time,
50
+ "last_time_elapsed" => @last_time_elapsed
51
+ }
52
+ end
53
+
54
+ def restore_state(data)
55
+ @queueId = data["queue_id"]
56
+ @job_id = data["job_id"]
57
+ @start_time = data["start_time"]
58
+ @last_time_elapsed = data["last_time_elapsed"]
59
+ end
60
+
32
61
  private
33
62
 
34
63
  def get_last_time_elapsed
35
- response = `curl -s #{ci_host}/lastStableBuild/api/xml`
64
+ retry_with_backoff do
65
+ response = `curl -s #{ci_host}/lastStableBuild/api/xml`
66
+ raise "Blank response from CI" if response.blank?
67
+ response
68
+ end
36
69
  response.match(/<duration>(\d+)<\/duration>/)
37
70
  $1 ? $1.to_i / 1000 : nil
71
+ rescue => e
72
+ puts " Warning: Could not get last build duration: #{e.message}"
73
+ nil
38
74
  end
39
75
 
40
76
  def auth
@@ -45,39 +81,23 @@ module Bard
45
81
  "http://#{auth}@ci.botandrose.com/job/#{project_name}"
46
82
  end
47
83
 
48
- def start
49
- command = "curl -s -I -X POST -L '#{ci_host}/buildWithParameters?GIT_REF=#{sha}'"
50
- output = `#{command}`
51
- @queueId = output[%r{Location: .+/queue/item/(\d+)/}, 1].to_i
52
- end
53
-
54
84
  def started?
55
- command = "curl -s -g '#{ci_host}/api/json?depth=1&tree=builds[queueId,number]'"
56
- output = `#{command}`
57
- JSON.parse(output)["builds"][0]["queueId"] == @queueId
85
+ retry_with_backoff do
86
+ command = "curl -s -g '#{ci_host}/api/json?depth=1&tree=builds[queueId,number]'"
87
+ output = `#{command}`
88
+ raise "Blank response from CI" if output.blank?
89
+ JSON.parse(output)["builds"][0]["queueId"] == @queueId
90
+ end
58
91
  end
59
92
 
60
93
  def job_id
61
94
  @job_id ||= begin
62
- output = `curl -s -g '#{ci_host}/api/json?depth=1&tree=builds[queueId,number]'`
63
- output[/"number":(\d+),"queueId":#{@queueId}\b/, 1].to_i
64
- end
65
- end
66
-
67
- def building?
68
- self.last_response = `curl -s #{ci_host}/#{job_id}/api/json?tree=building,result`
69
- if last_response.blank?
70
- sleep(2) # retry
71
- self.last_response = `curl -s #{ci_host}/#{job_id}/api/json?tree=building,result`
72
- if last_response.blank?
73
- raise "Blank response from CI twice in a row. Aborting!"
95
+ retry_with_backoff do
96
+ output = `curl -s -g '#{ci_host}/api/json?depth=1&tree=builds[queueId,number]'`
97
+ raise "Blank response from CI" if output.blank?
98
+ output[/"number":(\d+),"queueId":#{@queueId}\b/, 1].to_i
74
99
  end
75
100
  end
76
- last_response.include? '"building":true'
77
- end
78
-
79
- def success?
80
- last_response.include? '"result":"SUCCESS"'
81
101
  end
82
102
  end
83
103
  end
data/lib/bard/ci/local.rb CHANGED
@@ -1,24 +1,9 @@
1
1
  require "open3"
2
+ require "bard/ci/runner"
2
3
 
3
4
  module Bard
4
5
  class CI
5
- class Local < Struct.new(:project_name, :branch, :sha)
6
- def run
7
- start
8
-
9
- start_time = Time.new.to_i
10
- while building?
11
- elapsed_time = Time.new.to_i - start_time
12
- yield elapsed_time, nil
13
- sleep(2)
14
- end
15
-
16
- @stdin.close
17
- @console = @stdout_and_stderr.read
18
- @stdout_and_stderr.close
19
-
20
- success?
21
- end
6
+ class Local < Runner
22
7
 
23
8
  def exists?
24
9
  true
@@ -28,7 +13,7 @@ module Bard
28
13
  @console
29
14
  end
30
15
 
31
- private
16
+ protected
32
17
 
33
18
  def start
34
19
  @stdin, @stdout_and_stderr, @wait_thread = Open3.popen2e("CLEAN=true bin/rake ci")
@@ -41,6 +26,31 @@ module Bard
41
26
  def success?
42
27
  @wait_thread.value.success?
43
28
  end
29
+
30
+ def get_state_data
31
+ {
32
+ "project_name" => project_name,
33
+ "branch" => branch,
34
+ "start_time" => @start_time
35
+ }
36
+ end
37
+
38
+ def restore_state(data)
39
+ raise "Cannot resume local CI: process is no longer running. Start a new build with 'bard ci'."
40
+ end
41
+
42
+ def poll_until_complete
43
+ while building?
44
+ elapsed_time = Time.new.to_i - @start_time
45
+ yield elapsed_time, nil
46
+ save_state
47
+ sleep(2)
48
+ end
49
+
50
+ @stdin.close
51
+ @console = @stdout_and_stderr.read
52
+ @stdout_and_stderr.close
53
+ end
44
54
  end
45
55
  end
46
56
  end
@@ -0,0 +1,27 @@
1
+ module Bard
2
+ class CI
3
+ module Retryable
4
+ MAX_RETRIES = 5
5
+ INITIAL_DELAY = 1
6
+
7
+ def retry_with_backoff(max_retries: MAX_RETRIES)
8
+ retries = 0
9
+ delay = INITIAL_DELAY
10
+
11
+ begin
12
+ yield
13
+ rescue => e
14
+ if retries < max_retries
15
+ retries += 1
16
+ puts " Network error (attempt #{retries}/#{max_retries}): #{e.message}. Retrying in #{delay}s..."
17
+ sleep(delay)
18
+ delay *= 2
19
+ retry
20
+ else
21
+ raise "Network error after #{max_retries} attempts: #{e.message}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,78 @@
1
+ require "bard/ci/state"
2
+ require "bard/ci/retryable"
3
+
4
+ module Bard
5
+ class CI
6
+ class Runner < Struct.new(:project_name, :branch, :sha)
7
+ include Retryable
8
+
9
+ def run
10
+ start
11
+ @start_time = Time.new.to_i
12
+ @last_time_elapsed = get_last_time_elapsed
13
+ save_state
14
+ wait_until_started if respond_to?(:wait_until_started)
15
+
16
+ poll_until_complete { |elapsed, last_time| yield elapsed, last_time }
17
+
18
+ state.delete
19
+ success?
20
+ end
21
+
22
+ def resume
23
+ saved_state = state.load
24
+ raise "No saved CI state found for #{project_name}. Start a new build with 'bard ci'." if saved_state.nil?
25
+
26
+ restore_state(saved_state)
27
+ poll_until_complete { |elapsed, last_time| yield elapsed, last_time }
28
+
29
+ state.delete
30
+ success?
31
+ end
32
+
33
+ protected
34
+
35
+ def poll_until_complete
36
+ while building?
37
+ elapsed_time = Time.new.to_i - @start_time
38
+ yield elapsed_time, @last_time_elapsed
39
+ save_state
40
+ sleep(2)
41
+ end
42
+ end
43
+
44
+ def save_state
45
+ state.save(get_state_data)
46
+ end
47
+
48
+ def state
49
+ @state ||= State.new(project_name)
50
+ end
51
+
52
+ # Abstract methods - override in subclasses
53
+ def start
54
+ raise NotImplementedError, "#{self.class}#start not implemented"
55
+ end
56
+
57
+ def building?
58
+ raise NotImplementedError, "#{self.class}#building? not implemented"
59
+ end
60
+
61
+ def success?
62
+ raise NotImplementedError, "#{self.class}#success? not implemented"
63
+ end
64
+
65
+ def get_last_time_elapsed
66
+ nil
67
+ end
68
+
69
+ def get_state_data
70
+ raise NotImplementedError, "#{self.class}#get_state_data not implemented"
71
+ end
72
+
73
+ def restore_state(data)
74
+ raise NotImplementedError, "#{self.class}#restore_state not implemented"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Bard
5
+ class CI
6
+ class State
7
+ def initialize project_name
8
+ @project_name = project_name
9
+ end
10
+
11
+ def save data
12
+ FileUtils.mkdir_p(state_dir)
13
+ File.write(state_file, JSON.generate(data))
14
+ end
15
+
16
+ def load
17
+ return nil unless File.exist?(state_file)
18
+ JSON.parse(File.read(state_file))
19
+ end
20
+
21
+ def delete
22
+ File.delete(state_file) if File.exist?(state_file)
23
+ end
24
+
25
+ def exists?
26
+ File.exist?(state_file)
27
+ end
28
+
29
+ private
30
+
31
+ def state_dir
32
+ File.join(Dir.pwd, "tmp", "bard", "ci")
33
+ end
34
+
35
+ def state_file
36
+ File.join(state_dir, "#{@project_name}.json")
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/bard/ci.rb CHANGED
@@ -9,7 +9,7 @@ module Bard
9
9
  end
10
10
 
11
11
  extend Forwardable
12
- delegate [:run, :exists?, :console, :status] => :runner
12
+ delegate [:run, :resume, :exists?, :console, :status] => :runner
13
13
 
14
14
  private
15
15
 
data/lib/bard/cli/ci.rb CHANGED
@@ -7,30 +7,45 @@ module Bard::CLI::CI
7
7
 
8
8
  option :"local-ci", type: :boolean
9
9
  option :status, type: :boolean
10
+ option :resume, type: :boolean
10
11
  desc "ci [branch=HEAD]", "runs ci against BRANCH"
11
12
  def ci branch=Bard::Git.current_branch
12
13
  ci = Bard::CI.new(project_name, branch, local: options["local-ci"])
13
14
  if ci.exists?
14
15
  return puts ci.status if options["status"]
15
16
 
16
- puts "Continuous integration: starting build on #{branch}..."
17
+ if options["resume"]
18
+ puts "Continuous integration: resuming build..."
19
+ success = ci.resume do |elapsed_time, last_time|
20
+ if last_time
21
+ percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
22
+ output = " Estimated completion: #{percentage}%"
23
+ else
24
+ output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
25
+ end
26
+ print "\x08" * output.length
27
+ print output
28
+ $stdout.flush
29
+ end
30
+ else
31
+ puts "Continuous integration: starting build on #{branch}..."
17
32
 
18
- success = ci.run do |elapsed_time, last_time|
19
- if last_time
20
- percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
21
- output = " Estimated completion: #{percentage}%"
22
- else
23
- output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
33
+ success = ci.run do |elapsed_time, last_time|
34
+ if last_time
35
+ percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
36
+ output = " Estimated completion: #{percentage}%"
37
+ else
38
+ output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
39
+ end
40
+ print "\x08" * output.length
41
+ print output
42
+ $stdout.flush
24
43
  end
25
- print "\x08" * output.length
26
- print output
27
- $stdout.flush
28
44
  end
29
45
 
30
46
  if success
31
47
  puts
32
48
  puts "Continuous integration: success!"
33
- puts "Deploying..."
34
49
  else
35
50
  puts
36
51
  puts ci.console
data/lib/bard/github.rb CHANGED
@@ -2,9 +2,11 @@ require "net/http"
2
2
  require "json"
3
3
  require "base64"
4
4
  require "rbnacl"
5
+ require "bard/ci/retryable"
5
6
 
6
7
  module Bard
7
8
  class Github < Struct.new(:project_name)
9
+ include CI::Retryable
8
10
  def get path, params={}
9
11
  request(path) do |uri|
10
12
  uri.query = URI.encode_www_form(params)
@@ -117,26 +119,28 @@ module Bard
117
119
  end
118
120
  end
119
121
 
120
- req = nil
121
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
122
- req = block.call(uri)
123
- req["Accept"] = "application/vnd.github+json"
124
- req["Authorization"] = "Bearer #{github_apikey}"
125
- req["X-GitHub-Api-Version"] = "2022-11-28"
126
- http.request(req)
127
- end
122
+ retry_with_backoff do
123
+ req = nil
124
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
125
+ req = block.call(uri)
126
+ req["Accept"] = "application/vnd.github+json"
127
+ req["Authorization"] = "Bearer #{github_apikey}"
128
+ req["X-GitHub-Api-Version"] = "2022-11-28"
129
+ http.request(req)
130
+ end
128
131
 
129
- case response
130
- when Net::HTTPRedirection then
131
- Net::HTTP.get(URI(response["Location"]))
132
- when Net::HTTPSuccess then
133
- if response["Content-Type"].to_s.include?("/json")
134
- JSON.load(response.body)
132
+ case response
133
+ when Net::HTTPRedirection then
134
+ Net::HTTP.get(URI(response["Location"]))
135
+ when Net::HTTPSuccess then
136
+ if response["Content-Type"].to_s.include?("/json")
137
+ JSON.load(response.body)
138
+ else
139
+ response.body
140
+ end
135
141
  else
136
- response.body
142
+ raise [req.method, req.uri, req.to_hash, response, response.body].inspect
137
143
  end
138
- else
139
- raise [req.method, req.uri, req.to_hash, response, response.body].inspect
140
144
  end
141
145
  end
142
146
  end
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.4.10"
2
+ VERSION = "1.5.0"
3
3
  end
4
4
 
@@ -61,7 +61,6 @@ describe Bard::CLI::CI do
61
61
 
62
62
  expect(cli).to receive(:puts).with("Continuous integration: starting build on feature-branch...")
63
63
  expect(cli).to receive(:puts).with("Continuous integration: success!")
64
- expect(cli).to receive(:puts).with("Deploying...")
65
64
 
66
65
  cli.ci
67
66
  end
@@ -135,5 +134,30 @@ describe Bard::CLI::CI do
135
134
  cli.ci
136
135
  end
137
136
  end
137
+
138
+ context "with resume option" do
139
+ it "calls resume instead of run" do
140
+ allow(cli).to receive(:options).and_return({ "resume" => true })
141
+ allow(ci_runner).to receive(:exists?).and_return(true)
142
+ allow(ci_runner).to receive(:resume).and_yield(30, 60).and_return(true)
143
+
144
+ expect(cli).to receive(:puts).with("Continuous integration: resuming build...")
145
+ expect(cli).to receive(:puts).with("Continuous integration: success!")
146
+ expect(ci_runner).not_to receive(:run)
147
+
148
+ cli.ci
149
+ end
150
+
151
+ it "displays progress when resuming" do
152
+ allow(cli).to receive(:options).and_return({ "resume" => true })
153
+ allow(ci_runner).to receive(:exists?).and_return(true)
154
+ allow(ci_runner).to receive(:resume).and_yield(30, 60).and_return(true)
155
+
156
+ expect(cli).to receive(:print).with("\x08" * " Estimated completion: 50%".length)
157
+ expect(cli).to receive(:print).with(" Estimated completion: 50%")
158
+
159
+ cli.ci
160
+ end
161
+ end
138
162
  end
139
- end
163
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.10
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
@@ -164,6 +164,9 @@ files:
164
164
  - lib/bard/ci/github_actions.rb
165
165
  - lib/bard/ci/jenkins.rb
166
166
  - lib/bard/ci/local.rb
167
+ - lib/bard/ci/retryable.rb
168
+ - lib/bard/ci/runner.rb
169
+ - lib/bard/ci/state.rb
167
170
  - lib/bard/cli.rb
168
171
  - lib/bard/cli/ci.rb
169
172
  - lib/bard/cli/command.rb