released 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ module Released
2
+ class PipelineReader
3
+ def initialize(filename)
4
+ @filename = filename
5
+ end
6
+
7
+ def read
8
+ yaml = transform(YAML.load_file(@filename))
9
+
10
+ goals = []
11
+
12
+ yaml['goals'].each do |goal_yaml|
13
+ name = goal_yaml.keys.first
14
+ config = goal_yaml[name]
15
+ # TODO: what if there are more?
16
+
17
+ goals << Released::Goal.named(name.to_sym).new(config)
18
+ end
19
+
20
+ goals
21
+ end
22
+
23
+ private
24
+
25
+ def transform(obj)
26
+ case obj
27
+ when Hash
28
+ transform_hash(obj)
29
+ when Array
30
+ transform_array(obj)
31
+ when String
32
+ transform_string(obj)
33
+ else
34
+ obj
35
+ end
36
+ end
37
+
38
+ def transform_hash(hash)
39
+ hash.each_with_object({}) do |(key, value), memo|
40
+ memo[key] = transform(value)
41
+ end
42
+ end
43
+
44
+ def transform_array(array)
45
+ array.map do |elem|
46
+ transform(elem)
47
+ end
48
+ end
49
+
50
+ def transform_string(string)
51
+ case string
52
+ when /\Aenv!(.*)/
53
+ ENV.fetch(Regexp.last_match(1))
54
+ when /\Ash!(.*)/
55
+ `#{Regexp.last_match(1)}`
56
+ when /\A-----BEGIN PGP MESSAGE-----/
57
+ decrypt(string)
58
+ else
59
+ string
60
+ end
61
+ end
62
+
63
+ def decrypt(string)
64
+ stdout = ''
65
+ stderr = ''
66
+
67
+ piper = Nanoc::Extra::Piper.new(stdout: stdout, stderr: stderr)
68
+ piper.run(['gpg', '--decrypt', '--no-tty'], string)
69
+
70
+ stdout
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,171 @@
1
+ module Released
2
+ class Runner
3
+ # FIXME: extract UI
4
+
5
+ class TUI
6
+ def initialize(io)
7
+ @io = io
8
+ end
9
+
10
+ def move_up(num)
11
+ @io <<
12
+ if num == 1
13
+ "\e[A"
14
+ else
15
+ "\e[#{num}A"
16
+ end
17
+ end
18
+
19
+ def move_down(num)
20
+ @io <<
21
+ if num == 1
22
+ "\e[B"
23
+ else
24
+ "\e[#{num}B"
25
+ end
26
+ end
27
+
28
+ def move_to_left(col = 1)
29
+ @io <<
30
+ if col == 1
31
+ "\e[G"
32
+ else
33
+ "\e[#{col}G"
34
+ end
35
+ end
36
+
37
+ def clear_to_end_of_line
38
+ @io << "\e[K"
39
+ end
40
+ end
41
+
42
+ def initialize(goals, dry_run: false)
43
+ @goals = goals
44
+ @dry_run = dry_run
45
+
46
+ @tui = TUI.new($stdout)
47
+ @tui_mutex = Mutex.new
48
+ end
49
+
50
+ def run
51
+ assess_all
52
+ try_achieve_all
53
+ end
54
+
55
+ private
56
+
57
+ def print_goals
58
+ @goals.each do |goal|
59
+ left.times { $stdout << '. ' }
60
+
61
+ @tui.move_to_left
62
+ $stdout << goal
63
+ $stdout << ' '
64
+
65
+ @tui.move_to_left(left - 1)
66
+ $stdout << ' '
67
+
68
+ @tui.move_to_left
69
+ @tui.move_down(1)
70
+ end
71
+ end
72
+
73
+ def handle_error(e)
74
+ puts
75
+ puts 'FAILURE!'
76
+ puts '-----'
77
+ puts e.message
78
+ puts
79
+ puts e.backtrace.join("\n")
80
+ puts '-----'
81
+ puts 'Aborting!'
82
+ end
83
+
84
+ def write_state(idx, left, state)
85
+ up = @goals.size - idx
86
+ @tui.move_up(up)
87
+ @tui.move_to_left(left)
88
+ $stdout << state
89
+ @tui.clear_to_end_of_line
90
+ @tui.move_to_left
91
+ @tui.move_down(up)
92
+ end
93
+
94
+ def left
95
+ @_left ||= @goals.map { |g| g.to_s.size }.max + 5
96
+ end
97
+
98
+ def assess_all
99
+ puts 'Assessing goals…'
100
+ print_goals
101
+
102
+ @goals.each.with_index do |_, idx|
103
+ write_state(idx, left, 'waiting')
104
+ end
105
+
106
+ @goals.each.with_index do |goal, idx|
107
+ if goal.assessable?
108
+ write_state(idx, left, 'working…')
109
+
110
+ begin
111
+ goal.assess
112
+ write_state(idx, left, 'ok (succeeded)')
113
+ rescue => e
114
+ write_state(idx, left, 'failed')
115
+ handle_error(e)
116
+ exit 1 # FIXME: eww
117
+ end
118
+ else
119
+ write_state(idx, left, 'ok (skipped)')
120
+ end
121
+ end
122
+
123
+ puts
124
+ end
125
+
126
+ def try_achieve_all
127
+ puts 'Achieving goals…'
128
+ print_goals
129
+
130
+ @goals.each.with_index do |_, idx|
131
+ write_state(idx, left, 'waiting')
132
+ end
133
+
134
+ @goals.each.with_index do |goal, idx|
135
+ if @dry_run
136
+ if goal.achieved?
137
+ write_state(idx, left, 'ok (already achieved)')
138
+ else
139
+ write_state(idx, left, 'pending')
140
+ end
141
+ next
142
+ end
143
+
144
+ if goal.achieved?
145
+ write_state(idx, left, 'ok (already achieved)')
146
+ next
147
+ end
148
+
149
+ begin
150
+ write_state(idx, left, 'working…')
151
+ goal.try_achieve
152
+ rescue => e
153
+ write_state(idx, left, 'errored')
154
+ handle_error(e)
155
+ exit 1 # FIXME: eww
156
+ end
157
+
158
+ if !goal.effectful?
159
+ write_state(idx, left, 'ok (passed)')
160
+ next
161
+ elsif goal.achieved?
162
+ write_state(idx, left, 'ok (newly achieved)')
163
+ next
164
+ else
165
+ write_state(idx, left, 'failed')
166
+ exit 1 # FIXME: eww
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,3 @@
1
+ module Released
2
+ VERSION = '0.0.1'.freeze
3
+ end
data/lib/released.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Released
2
+ end
3
+
4
+ require 'English'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'shellwords'
8
+ require 'uri'
9
+
10
+ require 'ddplugin'
11
+ require 'nanoc'
12
+
13
+ require_relative 'released/version'
14
+ require_relative 'released/goal'
15
+ require_relative 'released/runner'
16
+ require_relative 'released/pipeline_reader'
17
+ require_relative 'released/goals'
data/released.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ require_relative 'lib/released/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'released'
5
+ s.version = Released::VERSION
6
+ s.homepage = 'https://github.com/ddfreyne/released'
7
+ s.summary = 'extensible release tool'
8
+ s.description = ''
9
+
10
+ s.author = 'Denis Defreyne'
11
+ s.email = 'denis.defreyne@stoneship.org'
12
+ s.license = 'MIT'
13
+
14
+ s.files =
15
+ Dir['[A-Z]*'] +
16
+ Dir['{lib,spec}/**/*'] +
17
+ Dir['*.gemspec']
18
+ s.require_paths = ['lib']
19
+
20
+ s.rdoc_options = ['--main', 'README.md']
21
+
22
+ s.required_ruby_version = '>= 2.3.0'
23
+
24
+ # Dependencies for goals
25
+ # TODO: Move into plugins
26
+ s.add_runtime_dependency('git')
27
+ s.add_runtime_dependency('twitter')
28
+ s.add_runtime_dependency('gems')
29
+ s.add_runtime_dependency('netrc')
30
+ s.add_runtime_dependency('octokit')
31
+
32
+ # TODO: Remove this (copy piper?)
33
+ s.add_runtime_dependency('nanoc', '~> 4.4')
34
+
35
+ s.add_runtime_dependency('ddplugin', '~> 1.0')
36
+
37
+ s.add_development_dependency('bundler', '>= 1.7.10', '< 2.0')
38
+ end
@@ -0,0 +1,64 @@
1
+ describe Released::Goals::GemBuilt do
2
+ subject(:goal) do
3
+ described_class.new(config)
4
+ end
5
+
6
+ let(:config) do
7
+ {
8
+ 'name' => 'donkey',
9
+ 'version' => '0.1',
10
+ }
11
+ end
12
+
13
+ let(:donkey_gemspec) do
14
+ <<~STRING
15
+ Gem::Specification.new do |s|
16
+ s.name = 'donkey'
17
+ s.version = '0.1'
18
+
19
+ s.summary = 'the cutest animal'
20
+ s.author = 'Denis Defreyne'
21
+ s.email = 'denis.defreyne@stoneship.org'
22
+ s.license = 'MIT'
23
+
24
+ s.files = []
25
+ end
26
+ STRING
27
+ end
28
+
29
+ describe '#achieved?' do
30
+ subject { goal.achieved? }
31
+
32
+ context 'file exists' do
33
+ before { File.write('donkey-0.1.gem', 'hello!') }
34
+ it { is_expected.to eql(true) }
35
+ end
36
+
37
+ context 'file does not exist' do
38
+ it { is_expected.to eql(false) }
39
+ end
40
+ end
41
+
42
+ describe '#try_achieve' do
43
+ subject { goal.try_achieve }
44
+
45
+ context 'no gemspec' do
46
+ it 'raises' do
47
+ expect { subject }.to raise_error(/Gemspec file not found/)
48
+ end
49
+ end
50
+
51
+ context 'valid gemspec' do
52
+ before { File.write('donkey.gemspec', donkey_gemspec) }
53
+
54
+ it 'builds the gem' do
55
+ expect { subject }.to change { File.file?('donkey-0.1.gem') }.from(false).to(true)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#failure_reason' do
61
+ subject { goal.failure_reason }
62
+ it { is_expected.to eql('expected the file donkey-0.1.gem to exist') }
63
+ end
64
+ end
@@ -0,0 +1,149 @@
1
+ describe Released::Goals::GemPushed, stdio: true do
2
+ subject(:goal) do
3
+ described_class.new(config)
4
+ end
5
+
6
+ let(:config) do
7
+ {
8
+ 'name' => gem_name,
9
+ 'version' => gem_version,
10
+ 'authorization' => authorization,
11
+ 'rubygems_base_url' => rubygems_base_url,
12
+ }
13
+ end
14
+
15
+ let(:gem_name) { 'nanoc' }
16
+ let(:gem_version) { '4.4.2' }
17
+ let(:authorization) { raise 'override me' }
18
+ let(:rubygems_base_url) { 'https://rubygems.org' }
19
+
20
+ let(:correct_authorization) { '83f5b7b9516c4342068cc60063a75de09bdb44d3' }
21
+ let(:incorrect_authorization) { '66666666666666666666666666666666' }
22
+
23
+ before do
24
+ gemspec =
25
+ <<~STRING
26
+ Gem::Specification.new do |s|
27
+ s.name = 'nanoc'
28
+ s.version = '4.4.2'
29
+
30
+ s.summary = 'the best thing ever'
31
+ s.author = 'Denis Defreyne'
32
+ s.email = 'denis.defreyne@stoneship.org'
33
+ s.license = 'MIT'
34
+
35
+ s.files = []
36
+ end
37
+ STRING
38
+
39
+ File.write('donkey.gemspec', gemspec)
40
+ system('gem', 'build', '--silent', 'donkey.gemspec')
41
+ end
42
+
43
+ describe '#assess' do
44
+ subject { goal.assess }
45
+
46
+ context 'incorrect authorization' do
47
+ let(:authorization) { incorrect_authorization }
48
+
49
+ it 'raises' do
50
+ VCR.use_cassette('goals__gem_pushed_spec__assess__incorrect_auth') do
51
+ expect { subject }.to raise_error(
52
+ RuntimeError, 'Authorization failed'
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ context 'correct authorization' do
59
+ let(:authorization) { correct_authorization }
60
+
61
+ context 'response does not include requested gem' do
62
+ let(:gem_name) { 'definitely_not_nanoc' }
63
+
64
+ it 'raises' do
65
+ VCR.use_cassette('goals__gem_pushed_spec__assess__correct_auth_but_gem_not_present') do
66
+ expect { subject }.to raise_error(
67
+ RuntimeError, 'List of owned gems does not include request gem'
68
+ )
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'response includes requested gem' do
74
+ it 'raises' do
75
+ VCR.use_cassette('goals__gem_pushed_spec__assess__correct_auth_and_gem_present') do
76
+ expect { subject }.not_to raise_error
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ describe '#achieved?' do
84
+ subject { goal.achieved? }
85
+
86
+ context 'incorrect authorization' do
87
+ let(:authorization) { incorrect_authorization }
88
+
89
+ it 'raises' do
90
+ VCR.use_cassette('goals__gem_pushed_spec___achieved_q__incorrect_auth') do
91
+ expect { subject }.to raise_error(
92
+ RuntimeError, 'Authorization failed'
93
+ )
94
+ end
95
+ end
96
+ end
97
+
98
+ context 'correct authorization' do
99
+ let(:authorization) { correct_authorization }
100
+
101
+ context 'achieved' do
102
+ it 'is achieved' do
103
+ VCR.use_cassette('goals__gem_pushed_spec___achieved_q__achieved') do
104
+ expect(subject).to be
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'not achieved' do
110
+ it 'is not achieved' do
111
+ VCR.use_cassette('goals__gem_pushed_spec___achieved_q__not_achieved') do
112
+ expect(subject).not_to be
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ describe '#try_achieve' do
120
+ subject { goal.try_achieve }
121
+
122
+ let(:rubygems_repo) do
123
+ Gems::Client.new(
124
+ key: '83f5b7b9516c4342068cc60063a75de09bdb44d3',
125
+ host: 'https://rubygems.org',
126
+ )
127
+ end
128
+
129
+ let(:authorization) { correct_authorization }
130
+
131
+ example do
132
+ VCR.use_cassette('goals__gem_pushed_spec___try_achieve__step_a') do
133
+ expect(rubygems_repo.gems.any? { |g| g['name'] == 'nanoc' && g['version'] == '4.4.2' }).not_to be
134
+ end
135
+
136
+ VCR.use_cassette('goals__gem_pushed_spec___try_achieve__step_b') do
137
+ subject
138
+ end
139
+
140
+ VCR.use_cassette('goals__gem_pushed_spec___try_achieve__step_c') do
141
+ expect(rubygems_repo.gems.any? { |g| g['name'] == 'nanoc' && g['version'] == '4.4.2' }).to be
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '#failure_reason' do
147
+ # TODO
148
+ end
149
+ end
@@ -0,0 +1,74 @@
1
+ describe Released::Goals::GitRefPushed do
2
+ subject(:goal) do
3
+ described_class.new(config)
4
+ end
5
+
6
+ let(:config) do
7
+ {
8
+ 'working_dir' => 'local',
9
+ 'remote' => 'gitlab',
10
+ 'branch' => 'devel',
11
+ }
12
+ end
13
+
14
+ let!(:local) do
15
+ Git.init('local').tap do |g|
16
+ g.config('user.name', 'Testy McTestface')
17
+ g.config('user.email', 'testface@example.com')
18
+
19
+ g.chdir { File.write('hello.txt', 'hi there') }
20
+ g.add('hello.txt')
21
+ g.commit('Add greeting')
22
+ g.branch('devel').checkout
23
+ end
24
+ end
25
+
26
+ let!(:remote) do
27
+ Git.init('remote')
28
+ end
29
+
30
+ before do
31
+ local.add_remote('gitlab', './remote')
32
+ end
33
+
34
+ describe '#achieved?' do
35
+ subject { goal.achieved? }
36
+
37
+ context 'not pushed' do
38
+ it { is_expected.not_to be }
39
+ end
40
+
41
+ context 'pushed, but not right rev' do
42
+ before do
43
+ goal.try_achieve
44
+
45
+ local.chdir { File.write('bye.txt', 'bye now') }
46
+ local.add('bye.txt')
47
+ local.commit('Add farewell')
48
+ local.branch('devel').checkout
49
+ end
50
+
51
+ it { is_expected.not_to be }
52
+ end
53
+
54
+ context 'pushed' do
55
+ before { goal.try_achieve }
56
+ it { is_expected.to be }
57
+ end
58
+ end
59
+
60
+ describe '#try_achieve' do
61
+ subject { goal.try_achieve }
62
+
63
+ example do
64
+ expect(remote.branches['devel']).to be_nil
65
+ subject
66
+ expect(remote.branches['devel'].gcommit.sha).to eql(local.branches['devel'].gcommit.sha)
67
+ end
68
+ end
69
+
70
+ describe '#failure_reason' do
71
+ subject { goal.failure_reason }
72
+ it { is_expected.to eql('HEAD does not exist on gitlab/devel') }
73
+ end
74
+ end
@@ -0,0 +1,49 @@
1
+ describe Released::PipelineReader do
2
+ subject(:pipeline_reader) { described_class.new(filename) }
3
+
4
+ let(:filename) { 'pipeline.yaml' }
5
+
6
+ before do
7
+ ENV['FAVORITE_GEM_AUTHOR'] = 'denis'
8
+ end
9
+
10
+ describe '#transform' do
11
+ subject { pipeline_reader.send(:transform, obj) }
12
+
13
+ context 'with array' do
14
+ let(:obj) { %w(hello env!FAVORITE_GEM_AUTHOR) }
15
+ it { is_expected.to eql(%w(hello denis)) }
16
+ end
17
+
18
+ context 'with hash' do
19
+ let(:obj) { { people: ['env!FAVORITE_GEM_AUTHOR'] } }
20
+ it { is_expected.to eql(people: ['denis']) }
21
+ end
22
+
23
+ context 'with string' do
24
+ context 'normal string' do
25
+ let(:obj) { 'hello' }
26
+ it { is_expected.to eql(obj) }
27
+ end
28
+
29
+ context 'env! string' do
30
+ let(:obj) { 'env!FAVORITE_GEM_AUTHOR' }
31
+ it { is_expected.to eql('denis') }
32
+ end
33
+
34
+ context 'sh! string' do
35
+ let(:obj) { 'sh!echo -n hello' }
36
+ it { is_expected.to eql('hello') }
37
+ end
38
+
39
+ context 'encrypted string' do
40
+ # TODO
41
+ end
42
+ end
43
+
44
+ context 'with anything else' do
45
+ let(:obj) { :donkey }
46
+ it { is_expected.to eql(obj) }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ require 'released'
2
+
3
+ require 'webmock/rspec'
4
+ require 'vcr'
5
+ require 'rspec/its'
6
+
7
+ VCR.configure do |config|
8
+ config.cassette_library_dir = 'fixtures/vcr_cassettes'
9
+ config.hook_into :webmock
10
+ end
11
+
12
+ RSpec.configure do |c|
13
+ c.around(:each, stdio: true) do |example|
14
+ orig_stdout = $stdout
15
+ orig_stderr = $stderr
16
+
17
+ $stdout = StringIO.new
18
+ $stderr = StringIO.new
19
+
20
+ example.run
21
+
22
+ $stdout = orig_stdout
23
+ $stderr = orig_stderr
24
+ end
25
+
26
+ c.around(:each) do |example|
27
+ Dir.mktmpdir('released-specs') do |dir|
28
+ FileUtils.cd(dir) do
29
+ example.run
30
+ end
31
+ end
32
+ end
33
+ end