flash_flow 1.0.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 +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +10 -0
- data/bin/flash_flow +23 -0
- data/flash_flow.gemspec +28 -0
- data/flash_flow.yml.erb.example +83 -0
- data/lib/flash_flow.rb +7 -0
- data/lib/flash_flow/branch_merger.rb +52 -0
- data/lib/flash_flow/cmd_runner.rb +37 -0
- data/lib/flash_flow/config.rb +71 -0
- data/lib/flash_flow/data.rb +6 -0
- data/lib/flash_flow/data/base.rb +58 -0
- data/lib/flash_flow/data/branch.rb +131 -0
- data/lib/flash_flow/data/collection.rb +181 -0
- data/lib/flash_flow/data/github.rb +129 -0
- data/lib/flash_flow/data/store.rb +33 -0
- data/lib/flash_flow/deploy.rb +184 -0
- data/lib/flash_flow/git.rb +248 -0
- data/lib/flash_flow/install.rb +19 -0
- data/lib/flash_flow/issue_tracker.rb +52 -0
- data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
- data/lib/flash_flow/lock.rb +25 -0
- data/lib/flash_flow/lock/github.rb +91 -0
- data/lib/flash_flow/notifier.rb +24 -0
- data/lib/flash_flow/notifier/hipchat.rb +36 -0
- data/lib/flash_flow/options.rb +36 -0
- data/lib/flash_flow/time_helper.rb +11 -0
- data/lib/flash_flow/version.rb +3 -0
- data/test/lib/data/test_base.rb +10 -0
- data/test/lib/data/test_branch.rb +203 -0
- data/test/lib/data/test_collection.rb +238 -0
- data/test/lib/data/test_github.rb +23 -0
- data/test/lib/data/test_store.rb +53 -0
- data/test/lib/issue_tracker/test_pivotal.rb +221 -0
- data/test/lib/lock/test_github.rb +70 -0
- data/test/lib/test_branch_merger.rb +76 -0
- data/test/lib/test_config.rb +84 -0
- data/test/lib/test_deploy.rb +175 -0
- data/test/lib/test_git.rb +73 -0
- data/test/lib/test_issue_tracker.rb +43 -0
- data/test/lib/test_notifier.rb +33 -0
- data/test/minitest_helper.rb +38 -0
- metadata +217 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
module FlashFlow
|
4
|
+
module Data
|
5
|
+
class TestGithub < Minitest::Test
|
6
|
+
|
7
|
+
class SomeFakeError < RuntimeError; end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
# @github = Github.new('fake_repo')
|
11
|
+
# @octokit = Minitest::Mock.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_cant_initialize_without_token
|
15
|
+
# val = ENV.delete('GH_TOKEN')
|
16
|
+
# assert_raises(RuntimeError) do
|
17
|
+
# Github.new('fake_repo')
|
18
|
+
# end
|
19
|
+
# ENV['GH_TOKEN'] = val
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'flash_flow/data/store'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
class TestStore < Minitest::Test
|
7
|
+
def sample_branches
|
8
|
+
{
|
9
|
+
'origin/branch 1' => {'branch' => 'branch 1', 'remote' => 'origin', 'status' => 'success', 'stories' => ['123']},
|
10
|
+
'other_origin/branch 2' => {'branch' => 'branch 2', 'remote' => 'origin', 'status' => 'success', 'stories' => ['456']}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@mock_git = MockGit.new
|
16
|
+
@collection = Collection.new({ 'origin' => 'the_origin_url' })
|
17
|
+
@branch = Branch.new('origin', 'the_origin_url', 'some_branch')
|
18
|
+
@storage = Store.new('/dev/null', @mock_git)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_get
|
22
|
+
@mock_git.stub(:read_file_from_merge_branch, JSON.pretty_generate(old_branches)) do
|
23
|
+
assert_equal(@storage.get, old_branches)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_write
|
28
|
+
str = StringIO.new
|
29
|
+
@storage.write(old_branches, str)
|
30
|
+
|
31
|
+
assert_equal(str.string.strip, JSON.pretty_generate(old_branches).strip)
|
32
|
+
end
|
33
|
+
|
34
|
+
private # Helpers
|
35
|
+
|
36
|
+
def old_branches
|
37
|
+
@old_branches ||= {
|
38
|
+
'the_origin_url/some_old_branch' => {'ref' => 'some_old_branch', 'remote_url' => 'the_origin_url', 'remote' => 'origin', 'created_at' => (Time.now - 3600).to_s, 'stories' => ['111']},
|
39
|
+
'the_origin_url/some_branch' => {'ref' => 'some_branch', 'remote_url' => 'the_origin_url', 'remote' => 'origin', 'status' => 'success', 'created_at' => (Time.now - 1800).to_s, 'stories' => ['222']}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MockGit
|
45
|
+
def read_file_from_merge_branch; end
|
46
|
+
def add_and_commit(_,_,_=nil); end
|
47
|
+
|
48
|
+
def in_temp_merge_branch
|
49
|
+
yield
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'flash_flow/issue_tracker/pivotal'
|
3
|
+
require 'flash_flow/time_helper'
|
4
|
+
|
5
|
+
module FlashFlow
|
6
|
+
module IssueTracker
|
7
|
+
class TestPivotal < Minitest::Test
|
8
|
+
include TimeHelper
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@project_mock = MiniTest::Mock.new
|
12
|
+
@stories = MiniTest::Mock.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_stories_pushed_only_marks_success_branches
|
16
|
+
stub_tracker_gem(@project_mock) do
|
17
|
+
[[0,'111'],[1,'222']].each do |branch, story|
|
18
|
+
mock_find(nil, story)
|
19
|
+
git = mock_working_branch(branch)
|
20
|
+
|
21
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
22
|
+
@stories.verify
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_stories_delivered_gets_success_and_removed_stories
|
28
|
+
stub_tracker_gem(@project_mock) do
|
29
|
+
mock_find(nil, '111')
|
30
|
+
mock_find(nil, '222')
|
31
|
+
mock_find(nil, '555')
|
32
|
+
|
33
|
+
Pivotal.new(sample_branches, nil).stories_delivered
|
34
|
+
@stories.verify
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_stories_delivered_only_delivers_finished_stories
|
39
|
+
stub_tracker_gem(@project_mock) do
|
40
|
+
story1_mock = MiniTest::Mock.new
|
41
|
+
.expect(:id, '111')
|
42
|
+
.expect(:current_state, 'finished')
|
43
|
+
.expect(:current_state=, true, ['delivered'])
|
44
|
+
.expect(:update, true)
|
45
|
+
story2_mock = MiniTest::Mock.new
|
46
|
+
.expect(:id, '222')
|
47
|
+
.expect(:current_state, 'delivered')
|
48
|
+
story3_mock = MiniTest::Mock.new
|
49
|
+
.expect(:id, '555')
|
50
|
+
.expect(:current_state, 'removed')
|
51
|
+
mock_find(story1_mock)
|
52
|
+
mock_find(story2_mock)
|
53
|
+
mock_find(story3_mock)
|
54
|
+
|
55
|
+
Pivotal.new(sample_branches, nil).stories_delivered
|
56
|
+
story1_mock.verify
|
57
|
+
story2_mock.verify
|
58
|
+
story3_mock.verify
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_stories_pushed_only_finishes_started_stories
|
63
|
+
stub_tracker_gem(@project_mock) do
|
64
|
+
story1_mock = MiniTest::Mock.new
|
65
|
+
.expect(:id, '111')
|
66
|
+
.expect(:current_state, 'started')
|
67
|
+
.expect(:current_state=, true, ['finished'])
|
68
|
+
.expect(:update, true)
|
69
|
+
story2_mock = MiniTest::Mock.new
|
70
|
+
.expect(:id, '222')
|
71
|
+
.expect(:current_state, 'finished')
|
72
|
+
mock_find(story1_mock)
|
73
|
+
mock_find(story2_mock)
|
74
|
+
|
75
|
+
[[0,story1_mock],[1,story2_mock]].each do |branch, story|
|
76
|
+
git = mock_working_branch(branch)
|
77
|
+
|
78
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
79
|
+
story.verify
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_stories_pushed_only_finishes_stories_of_current_branch
|
85
|
+
stub_tracker_gem(@project_mock) do
|
86
|
+
story1_mock = MiniTest::Mock.new
|
87
|
+
.expect(:id, '111')
|
88
|
+
.expect(:current_state, 'started')
|
89
|
+
.expect(:current_state=, true, ['finished'])
|
90
|
+
.expect(:update, true)
|
91
|
+
story2_mock = MiniTest::Mock.new
|
92
|
+
.expect(:id, '222')
|
93
|
+
mock_find(story1_mock)
|
94
|
+
mock_find(story2_mock)
|
95
|
+
|
96
|
+
git = mock_working_branch(0)
|
97
|
+
|
98
|
+
Pivotal.new(sample_branches, git).stories_pushed
|
99
|
+
story1_mock.verify
|
100
|
+
story2_mock.verify
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_production_deploy_only_comments_on_shipped_branches
|
105
|
+
stub_tracker_gem(@project_mock) do
|
106
|
+
mock_find(nil, '111')
|
107
|
+
|
108
|
+
Pivotal.new(sample_branches, mock_git).production_deploy
|
109
|
+
@stories.verify
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_production_deploy_comments
|
114
|
+
shipped_text = with_time_zone("US/Pacific") { Time.now.strftime("Shipped to production on %m/%d/%Y at %H:%M") }
|
115
|
+
fake_notes = Minitest::Mock.new
|
116
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment('Some other random comment')])
|
117
|
+
.expect(:create, true, [{ text: shipped_text }])
|
118
|
+
story_mock = MiniTest::Mock.new
|
119
|
+
.expect(:id, '111')
|
120
|
+
.expect(:notes, fake_notes)
|
121
|
+
.expect(:notes, fake_notes)
|
122
|
+
|
123
|
+
stub_tracker_gem(@project_mock) do
|
124
|
+
mock_find(story_mock)
|
125
|
+
|
126
|
+
Pivotal.new(sample_branches, mock_git, {'timezone' => "US/Pacific"}).production_deploy
|
127
|
+
end
|
128
|
+
|
129
|
+
story_mock.verify
|
130
|
+
fake_notes.verify
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_production_deploy_only_comments_if_no_existing_comment
|
134
|
+
fake_notes = Minitest::Mock.new
|
135
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment('Shipped to production on')])
|
136
|
+
story_mock = MiniTest::Mock.new
|
137
|
+
.expect(:id, '111')
|
138
|
+
.expect(:notes, fake_notes)
|
139
|
+
|
140
|
+
stub_tracker_gem(@project_mock) do
|
141
|
+
mock_find(story_mock)
|
142
|
+
|
143
|
+
Pivotal.new(sample_branches, mock_git).production_deploy
|
144
|
+
end
|
145
|
+
|
146
|
+
story_mock.verify
|
147
|
+
fake_notes.verify
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_list_release_notes_with_time_scope
|
151
|
+
time = Time.now
|
152
|
+
time -= time.sec
|
153
|
+
time_now = time.strftime("%m/%d/%Y at %H:%M")
|
154
|
+
fake_notes = Minitest::Mock.new
|
155
|
+
.expect(:all, [mock_comment('Some random comment'), mock_comment("Shipped to production on #{time_now}")])
|
156
|
+
story_mock = MiniTest::Mock.new
|
157
|
+
.expect(:id, '111')
|
158
|
+
.expect(:name, 'fake_name')
|
159
|
+
.expect(:notes, fake_notes)
|
160
|
+
fake_file = MiniTest::Mock.new
|
161
|
+
.expect(:puts, nil, ["PT#111 fake_name (#{time})"])
|
162
|
+
|
163
|
+
stub_tracker_gem(@project_mock) do
|
164
|
+
pivotal = Pivotal.new(sample_branches, mock_git)
|
165
|
+
pivotal.stub(:done_and_current_stories, [story_mock]) do
|
166
|
+
pivotal.release_notes(24, fake_file)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
story_mock.verify
|
171
|
+
fake_notes.verify
|
172
|
+
fake_file.verify
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def stub_tracker_gem(project)
|
178
|
+
PivotalTracker::Client.stub(:token=, true) do
|
179
|
+
PivotalTracker::Client.stub(:use_ssl=, true) do
|
180
|
+
PivotalTracker::Project.stub(:find, project) do
|
181
|
+
yield
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def mock_git
|
188
|
+
Minitest::Mock.new
|
189
|
+
.expect(:master_branch_contains?, true, [sample_branches[0].sha])
|
190
|
+
.expect(:master_branch_contains?, false, [sample_branches[1].sha])
|
191
|
+
.expect(:master_branch_contains?, false, [sample_branches[2].sha])
|
192
|
+
.expect(:master_branch_contains?, false, [sample_branches[3].sha])
|
193
|
+
.expect(:master_branch_contains?, false, [sample_branches[4].sha])
|
194
|
+
end
|
195
|
+
|
196
|
+
def mock_working_branch(index)
|
197
|
+
git = Minitest::Mock.new
|
198
|
+
5.times { git.expect(:working_branch, sample_branches[index].ref) }
|
199
|
+
git
|
200
|
+
end
|
201
|
+
|
202
|
+
def mock_comment(comment)
|
203
|
+
Minitest::Mock.new.expect(:text, comment)
|
204
|
+
end
|
205
|
+
|
206
|
+
def mock_find(story, story_id=nil)
|
207
|
+
story_id ||= story.id
|
208
|
+
@project_mock.expect(:stories, @stories.expect(:find, story, [story_id]))
|
209
|
+
end
|
210
|
+
|
211
|
+
def sample_branches
|
212
|
+
@sample_branches ||= [Data::Branch.from_hash({'ref' => 'branch1', 'remote' => 'origin', 'sha' => 'sha1', 'status' => 'success', 'created_at' => (Time.now - 3600), 'stories' => ['111']}),
|
213
|
+
Data::Branch.from_hash({'ref' => 'branch2', 'remote' => 'origin', 'sha' => 'sha2', 'status' => 'success', 'created_at' => (Time.now - 1800), 'stories' => ['222']}),
|
214
|
+
Data::Branch.from_hash({'ref' => 'branch3', 'remote' => 'origin', 'sha' => 'sha3', 'status' => 'fail', 'created_at' => (Time.now - 1800), 'stories' => ['333']}),
|
215
|
+
Data::Branch.from_hash({'ref' => 'branch4', 'remote' => 'origin', 'sha' => 'sha4', 'status' => nil, 'created_at' => (Time.now - 1800), 'stories' => ['444']}),
|
216
|
+
Data::Branch.from_hash({'ref' => 'branch5', 'remote' => 'origin', 'sha' => 'sha5', 'status' => 'removed', 'created_at' => (Time.now - 1800), 'stories' => ['555']})
|
217
|
+
]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'minitest/stub_any_instance'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Lock
|
6
|
+
class TestGithub < Minitest::Test
|
7
|
+
class SomeFakeError < RuntimeError;
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@lock = Lock::Github.new(params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_raises_without_required_params
|
15
|
+
config = params.select { |k, _| k != 'token' }
|
16
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
17
|
+
|
18
|
+
config = params.select { |k, _| k != 'repo' }
|
19
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
20
|
+
|
21
|
+
config = params.select { |k, _| k != 'issue_id' }
|
22
|
+
assert_raises(Lock::Error) { Lock::Github.new(config) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_error_message_when_issue_opened
|
26
|
+
@lock.stub(:issue_open?, true) do
|
27
|
+
@lock.stub(:get_last_event, {actor: {login: 'anonymous'}, created_at: Time.now}) do
|
28
|
+
assert_raises(FlashFlow::Lock::Error) do
|
29
|
+
@lock.with_lock
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_with_lock_calls_the_block
|
36
|
+
my_mock = Minitest::Mock.new.expect(:block_call, true).expect(:close_issue, true)
|
37
|
+
|
38
|
+
@lock.stub(:issue_open?, false) do
|
39
|
+
@lock.stub(:open_issue, nil) do
|
40
|
+
@lock.stub(:close_issue, -> { my_mock.close_issue }) do
|
41
|
+
@lock.with_lock { my_mock.block_call }
|
42
|
+
my_mock.verify
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_with_lock_closes_issue_no_matter_what
|
49
|
+
my_mock = Minitest::Mock.new.expect(:some_method, true)
|
50
|
+
|
51
|
+
@lock.stub(:issue_open?, false) do
|
52
|
+
@lock.stub(:open_issue, nil) do
|
53
|
+
@lock.stub(:close_issue, -> { my_mock.some_method }) do
|
54
|
+
assert_raises(SomeFakeError) do
|
55
|
+
@lock.with_lock { raise SomeFakeError }
|
56
|
+
my_mock.verify
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def params
|
66
|
+
{'token' => '1234567890', 'repo' => 'f/f', 'issue_id' => '123'}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
require 'minitest/stub_any_instance'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
class TestBranchMerger < Minitest::Test
|
6
|
+
|
7
|
+
def setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_deleted_branch
|
11
|
+
merger.stub(:sha, nil) do
|
12
|
+
assert_equal(merger.do_merge(true), :deleted)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_successful_merge
|
17
|
+
git.expect(:last_success?, true)
|
18
|
+
|
19
|
+
merger.stub(:sha, 'some_sha') do
|
20
|
+
assert_equal(merger.do_merge(false), :success)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_successful_rerere
|
25
|
+
git.expect(:last_success?, false)
|
26
|
+
.expect(:rerere_resolve!, true)
|
27
|
+
|
28
|
+
merger.stub(:sha, 'some_sha') do
|
29
|
+
assert_equal(merger.do_merge(false), :success)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_rerere_forget
|
34
|
+
git.expect(:last_success?, false)
|
35
|
+
.expect(:run, true, [ 'rerere forget' ])
|
36
|
+
.expect(:run, true, [ 'reset --hard HEAD' ])
|
37
|
+
.expect(:run, true, [ 'rev-parse HEAD' ])
|
38
|
+
.expect(:last_stdout, 'conflict sha', )
|
39
|
+
|
40
|
+
merger.stub(:sha, 'some_sha') do
|
41
|
+
assert_equal(merger.do_merge(true), :conflict)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_failed_rerere
|
46
|
+
git.expect(:last_success?, false)
|
47
|
+
.expect(:rerere_resolve!, false)
|
48
|
+
.expect(:run, true, [ 'reset --hard HEAD' ])
|
49
|
+
.expect(:run, true, [ 'rev-parse HEAD' ])
|
50
|
+
.expect(:last_stdout, 'conflict sha', )
|
51
|
+
|
52
|
+
merger.stub(:sha, 'some_sha') do
|
53
|
+
assert_equal(merger.do_merge(false), :conflict)
|
54
|
+
assert_equal(merger.conflict_sha, 'conflict sha')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def merger
|
61
|
+
@merger ||= BranchMerger.new(git, branch)
|
62
|
+
end
|
63
|
+
|
64
|
+
def branch
|
65
|
+
@branch ||= Data::Branch.from_hash({'ref' => 'pushing_branch', 'remote' => 'origin', 'status' => 'fail', 'stories' => []})
|
66
|
+
end
|
67
|
+
|
68
|
+
def git
|
69
|
+
return @git if @git
|
70
|
+
|
71
|
+
@git = Minitest::Mock.new
|
72
|
+
@git.expect(:run, true, ["merge --no-ff #{branch.remote}/#{branch.ref}"])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|