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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +152 -0
  8. data/Rakefile +10 -0
  9. data/bin/flash_flow +23 -0
  10. data/flash_flow.gemspec +28 -0
  11. data/flash_flow.yml.erb.example +83 -0
  12. data/lib/flash_flow.rb +7 -0
  13. data/lib/flash_flow/branch_merger.rb +52 -0
  14. data/lib/flash_flow/cmd_runner.rb +37 -0
  15. data/lib/flash_flow/config.rb +71 -0
  16. data/lib/flash_flow/data.rb +6 -0
  17. data/lib/flash_flow/data/base.rb +58 -0
  18. data/lib/flash_flow/data/branch.rb +131 -0
  19. data/lib/flash_flow/data/collection.rb +181 -0
  20. data/lib/flash_flow/data/github.rb +129 -0
  21. data/lib/flash_flow/data/store.rb +33 -0
  22. data/lib/flash_flow/deploy.rb +184 -0
  23. data/lib/flash_flow/git.rb +248 -0
  24. data/lib/flash_flow/install.rb +19 -0
  25. data/lib/flash_flow/issue_tracker.rb +52 -0
  26. data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
  27. data/lib/flash_flow/lock.rb +25 -0
  28. data/lib/flash_flow/lock/github.rb +91 -0
  29. data/lib/flash_flow/notifier.rb +24 -0
  30. data/lib/flash_flow/notifier/hipchat.rb +36 -0
  31. data/lib/flash_flow/options.rb +36 -0
  32. data/lib/flash_flow/time_helper.rb +11 -0
  33. data/lib/flash_flow/version.rb +3 -0
  34. data/test/lib/data/test_base.rb +10 -0
  35. data/test/lib/data/test_branch.rb +203 -0
  36. data/test/lib/data/test_collection.rb +238 -0
  37. data/test/lib/data/test_github.rb +23 -0
  38. data/test/lib/data/test_store.rb +53 -0
  39. data/test/lib/issue_tracker/test_pivotal.rb +221 -0
  40. data/test/lib/lock/test_github.rb +70 -0
  41. data/test/lib/test_branch_merger.rb +76 -0
  42. data/test/lib/test_config.rb +84 -0
  43. data/test/lib/test_deploy.rb +175 -0
  44. data/test/lib/test_git.rb +73 -0
  45. data/test/lib/test_issue_tracker.rb +43 -0
  46. data/test/lib/test_notifier.rb +33 -0
  47. data/test/minitest_helper.rb +38 -0
  48. 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