deployment_pipeline 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +16 -0
- data/Gemfile.lock +40 -0
- data/README.md +90 -0
- data/bin/pipeline +202 -0
- data/deployment_pipeline.gemspec +22 -0
- data/extra/rugged-0.16.0.gem +0 -0
- data/lib/code_repository.rb +45 -0
- data/lib/pipeline.rb +17 -0
- data/lib/tracker.rb +102 -0
- data/sample.pipeline_config +14 -0
- data/spec/code_repository_spec.rb +30 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/tracker_spec.rb +48 -0
- metadata +141 -0
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source :rubygems
|
2
|
+
gem "thor","0.14.6"
|
3
|
+
gem "pg"
|
4
|
+
gem "active_support"
|
5
|
+
gem "i18n"
|
6
|
+
gem "chronic"
|
7
|
+
gem "rdiscount"
|
8
|
+
gem "rugged","0.16.0"
|
9
|
+
gem "progressbar","0.11.0"
|
10
|
+
gem "hpricot"
|
11
|
+
|
12
|
+
#testing related
|
13
|
+
gem "rspec"
|
14
|
+
gem "rr","1.0.4"
|
15
|
+
|
16
|
+
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
active_support (3.0.0)
|
5
|
+
activesupport (= 3.0.0)
|
6
|
+
activesupport (3.0.0)
|
7
|
+
chronic (0.6.7)
|
8
|
+
diff-lcs (1.1.3)
|
9
|
+
hpricot (0.8.6)
|
10
|
+
i18n (0.6.0)
|
11
|
+
pg (0.13.2)
|
12
|
+
progressbar (0.11.0)
|
13
|
+
rdiscount (1.6.8)
|
14
|
+
rr (1.0.4)
|
15
|
+
rspec (2.10.0)
|
16
|
+
rspec-core (~> 2.10.0)
|
17
|
+
rspec-expectations (~> 2.10.0)
|
18
|
+
rspec-mocks (~> 2.10.0)
|
19
|
+
rspec-core (2.10.1)
|
20
|
+
rspec-expectations (2.10.0)
|
21
|
+
diff-lcs (~> 1.1.3)
|
22
|
+
rspec-mocks (2.10.1)
|
23
|
+
rugged (0.16.0)
|
24
|
+
thor (0.14.6)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
active_support
|
31
|
+
chronic
|
32
|
+
hpricot
|
33
|
+
i18n
|
34
|
+
pg
|
35
|
+
progressbar (= 0.11.0)
|
36
|
+
rdiscount
|
37
|
+
rr (= 1.0.4)
|
38
|
+
rspec
|
39
|
+
rugged (= 0.16.0)
|
40
|
+
thor (= 0.14.6)
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
Deployment Pipeline (Let's make Continuous Deployment painless)
|
3
|
+
-------------------
|
4
|
+
|
5
|
+
In the life of Deployment Manager of an agile aggressive team<sup>1</sup>, there comes a day when he/she needs to manage releases and communicate with
|
6
|
+
stake holders about features being released and also control what goes live on production. Why control? because in any practical scenario, feature which is ready
|
7
|
+
and accepted by product managers doesn't guarantee its readiness for the business and marketing and quite often non-technical teams need a buffer time before
|
8
|
+
a feature is released. Deployment Pipeline is here to help you manage these releases and keep non-technical teams and users better informed about whats going on.
|
9
|
+
|
10
|
+
|
11
|
+
### Ideal Scenario:
|
12
|
+
|
13
|
+
1. Your Product Managers write feature/bug stories and prioritize them anytime during the day.
|
14
|
+
2. Your engineering team believes in Continuous Integration and all engineers commit to Master branch (or Trunk) several times a day.
|
15
|
+
3. If the build (CI) is green, that tag from Master Branch might get pushed to QA environment for stories to be delivered.
|
16
|
+
4. Once all stories are accepted, (QA) Staging Tag is deployed to production, Happy Ending of the day!
|
17
|
+
|
18
|
+
### Practical Scenario:
|
19
|
+
1. You have 5 Product Managers who requests feature/bug stories and prioritize them anytime during the day.
|
20
|
+
2. You have 10 engineers working on 5 stories and commits to Master branch (or Trunk) several times a day.
|
21
|
+
3. Build is green only 60% of time during the day.
|
22
|
+
4. By the time build is green there are commits to 4 finished & intermediate commits to an 1 un-finished story.
|
23
|
+
5. QA delivers the stories(4) which has been finished and 3 are accepted and 1 is rejected.
|
24
|
+
6. Among 3 accepted stories Marketing Team takes a call to hold 1 story even though its ready.
|
25
|
+
7. Now we have 2 production ready, 1 held by marketing, 1 rejected, 1 un-finished.
|
26
|
+
8. Commits related to 2 prod ready are shuffled between commits related to all non-ready stories.
|
27
|
+
9. Among 2 ready-to-deploy , 1 is marked urgent but it's commits are part of the day when CI build was RED (not necessarily due to this commit).
|
28
|
+
10. What and how would you deploy today? (cherry-pick commits for a feature with no green build? FAIL) You post-pone release!
|
29
|
+
- ==Day Rolls Over==
|
30
|
+
11. 3 new stories requested by Product Managers.
|
31
|
+
12. An engineer finishes 2 of new stories quickly whose commits goes to master. 3rd new story might take long to finish, but gets its commits pushed to master.
|
32
|
+
13. Build is green and a tag on master is pushed to QA environment.
|
33
|
+
14. 2 new stories are delivered and accepted.
|
34
|
+
15. ...
|
35
|
+
16. Which staging tag on master would you deploy to production? At any given time there are commits from un-finished/un-delivered stories.
|
36
|
+
|
37
|
+
|
38
|
+
### Solutions:
|
39
|
+
1. Sure you can use [feature toggle](http://martinfowler.com/bliki/FeatureToggle.html), but it only makes sense for long running (for weeks) set of stories. When every story starts to have a feature toggle, then system gets polluted with
|
40
|
+
if-else everywhere, which again is difficult to manage and error prone
|
41
|
+
2. You can also ask engineers to have separate feature branches for each stories and rebase with master often. This brings in its own [over heads](http://martinfowler.com/bliki/FeatureBranch.html)...
|
42
|
+
* Time spent in merging changes
|
43
|
+
* It needs a single controller of release branch who pulls the changes and makes sure what goes live is vetted. (This controller can soon become a bottleneck in the process)
|
44
|
+
* Engineers work in isolation and can not commit intermediate commits unless feature is complete.
|
45
|
+
3. Use All-Accepted Marker : This is a commit on master below which all stories have been accepted and there are few commits (shuffled with other un-finished) above the marker that can be cleanly cherry-picked.
|
46
|
+
|
47
|
+
|
48
|
+
#### Workflow for All-Accepted Marker Deployment:
|
49
|
+
1. Find a suitable commit below which all stories are accepted
|
50
|
+
2. Branch out to new "Release" Branch
|
51
|
+
3. Inform stake-holders about what features are being released and locked down release marker
|
52
|
+
4. Cherry-pick related commits from above the marker to release branch
|
53
|
+
5. Build the release branch and wait for it to be green
|
54
|
+
6. Deploy release branch to production
|
55
|
+
7. Automate this entire process
|
56
|
+
|
57
|
+
### Deployment Pipline @ Work :
|
58
|
+
####Getting Started:
|
59
|
+
<pre><code>
|
60
|
+
developers-machine:~/workspace/repository (master)$ <b>pipeline help</b>
|
61
|
+
Tasks:
|
62
|
+
pipeline help [TASK] # Describe available tasks or one specific task
|
63
|
+
pipeline release_plan # Prepares a release plan
|
64
|
+
pipeline setup # Setup Deployment Pipeline Tool
|
65
|
+
pipeline status # lists all stories with their status
|
66
|
+
pipeline suitable_release # Suggests a release commit to be picked and also includes a release plan
|
67
|
+
|
68
|
+
Options:
|
69
|
+
[--config=CONFIG] # A ruby file that defines relevant constants & configs. accepts ENV $PIPELINE_CONFIG
|
70
|
+
# Default: /Users/dev_home/.pipeline_config
|
71
|
+
</code></pre>
|
72
|
+
|
73
|
+
|
74
|
+
####Find Suitable Commit for All-Accepted Marker:
|
75
|
+
<pre><code>
|
76
|
+
developers-machine:~/workspace/repository (master)$ <b>pipeline help</b>
|
77
|
+
Tasks:
|
78
|
+
pipeline help [TASK] # Describe available tasks or one specific task
|
79
|
+
pipeline release_plan # Prepares a release plan
|
80
|
+
pipeline setup # Setup Deployment Pipeline Tool
|
81
|
+
pipeline status # lists all stories with their status
|
82
|
+
pipeline suitable_release # Suggests a release commit to be picked and also includes a release plan
|
83
|
+
|
84
|
+
Options:
|
85
|
+
[--config=CONFIG] # A ruby file that defines relevant constants & configs. accepts ENV $PIPELINE_CONFIG
|
86
|
+
# Default: /Users/dev_home/.pipeline_config
|
87
|
+
</code></pre>
|
88
|
+
|
89
|
+
<sub>\[**1**\]: Team which is motivated for [release-often philosophy](http://radar.oreilly.com/2009/03/continuous-deployment-5-eas.html) so much that it releases to production multiple times a day. It uses DVCS like **[Git](http://git-scm.com)** and agile story tracker like **[PIVOTAL TRACKER](http://www.pivotaltracker.com)**. It has adopted TDD & **[Continious Integration](http://en.wikipedia.org/wiki/Continuous_integration)** as way of life. Every engineer [commits to master all the time](http://martinfowler.com/bliki/FeatureBranch.html#PromiscuousIntegrationVsContinuousIntegration).
|
90
|
+
</sub>
|
data/bin/pipeline
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../lib/pipeline'
|
4
|
+
|
5
|
+
class PipelineCmd < Thor
|
6
|
+
class_option :config, :type => :string,
|
7
|
+
:desc => "A ruby file that defines relevant constants & configs. accepts ENV $PIPELINE_CONFIG",
|
8
|
+
:default => ENV["PIPELINE_CONFIG"] || "#{ENV['HOME']}/.pipeline_config"
|
9
|
+
def initialize(args=[], options={}, config={})
|
10
|
+
super
|
11
|
+
load(self.options[:config])
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "setup", "Setup Deployment Pipeline Tool"
|
15
|
+
method_option :force, :type => :boolean, :default => false, :desc => "Force operation"
|
16
|
+
def setup
|
17
|
+
#stub
|
18
|
+
puts "Setup complete!"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
desc "status", "lists all stories with their status"
|
23
|
+
method_option :repository_path, :default => "#{Dir.pwd}", :desc => "Git repository path"
|
24
|
+
method_option :branch, :default => "master", :desc => "Git branch to consider"
|
25
|
+
method_option :commit_range, :type => :string, :desc => "Range of commits, eg.\"last_release_tag1..HEAD\""
|
26
|
+
method_option :html, :type => :boolean, :default => false, :desc => "pretty html output"
|
27
|
+
def status
|
28
|
+
|
29
|
+
c_range = options[:commit_range]
|
30
|
+
c_range = c_range.split("..")
|
31
|
+
|
32
|
+
md_msg = ""
|
33
|
+
|
34
|
+
code_repo = CodeRepository.new(options[:repository_path],c_range[0],c_range[1])
|
35
|
+
tracker = PivotalTracker.new
|
36
|
+
|
37
|
+
stories,untagged_commits = tracker.extract_story_ids(code_repo.commits.reverse)
|
38
|
+
|
39
|
+
|
40
|
+
tracker.load_stories(stories.keys,true)
|
41
|
+
|
42
|
+
stories_by_status = {}
|
43
|
+
tracker.stories.each {|story|
|
44
|
+
stories_by_status[story[:status]] ||= []
|
45
|
+
stories_by_status[story[:status]] << " * (#{story[:type]}) [#{story[:name]}](#{story[:url]}) requested by **#{story[:requested_by]}** owned by #{code_repo.contributors(stories[story[:id]]).join(', ')} "
|
46
|
+
}
|
47
|
+
|
48
|
+
stories_by_status.each_pair do |status,stories|
|
49
|
+
md_msg << "\n#{status}: \n\n"
|
50
|
+
stories.each {|s| md_msg << " #{s} \n"}
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
if untagged_commits.length > 0
|
57
|
+
md_msg << "\n Following commits have not been tagged, (**why lah?**) \n\n"
|
58
|
+
untagged_commits.each do |c|
|
59
|
+
md_msg << " * #{c.oid} #{c.message.gsub("\n"," ")[0...140]} by **#{c.author[:name]}** \n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
puts "STATUS:"
|
64
|
+
puts md_msg
|
65
|
+
if options[:html] == true
|
66
|
+
markdown = RDiscount.new(md_msg)
|
67
|
+
temp_html_file = "/tmp/tmp_msg_#{Time.now.to_i}.html"
|
68
|
+
File.open(temp_html_file, 'w') {|f| f.write(markdown.to_html) }
|
69
|
+
system("open #{temp_html_file}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "suitable_release", "Suggests a release commit to be picked and also includes a release plan"
|
74
|
+
method_option :repository_path, :default => "#{Dir.pwd}", :desc => "Git repository path"
|
75
|
+
method_option :branch, :default => "master", :desc => "Git branch to consider"
|
76
|
+
method_option :last_release_commit, :type => :string, :desc => "Commit id (SHA) of previous release "
|
77
|
+
def suitable_release
|
78
|
+
last_release_commit = options[:last_release_commit]
|
79
|
+
code_repo = CodeRepository.new(options[:repository_path],last_release_commit)
|
80
|
+
tracker = PivotalTracker.new
|
81
|
+
|
82
|
+
suitable_release_sha = last_release_commit
|
83
|
+
|
84
|
+
code_repo.commits.reverse.each do |commit|
|
85
|
+
puts "Analysing... #{commit.oid}"
|
86
|
+
#Check if all stories associated to this commit has been accepted, if yes consider the commit , else break
|
87
|
+
#Consider the commit if its untagged anyway
|
88
|
+
commit_suitable = true
|
89
|
+
story_ids,untagged = tracker.extract_story_ids([commit]) #this will return story_ids and untagged such that they are mutually exclusive.
|
90
|
+
story_ids.keys.each { |story_id|
|
91
|
+
story = tracker.story_obj(story_id)
|
92
|
+
if story.nil? || story[:status] != "accepted"
|
93
|
+
puts "Unknown story :#{story_id} " if story.nil?
|
94
|
+
puts "Unsuitable commit because story status is #{story[:status]}"
|
95
|
+
commit_suitable = false
|
96
|
+
break
|
97
|
+
end
|
98
|
+
|
99
|
+
suitable_release_sha = commit.oid
|
100
|
+
puts "Stories accepted for #{commit.oid}, hence considered"
|
101
|
+
}
|
102
|
+
unless untagged.empty?
|
103
|
+
puts "Commit is untagged hence considered"
|
104
|
+
suitable_release_sha = untagged.first.oid
|
105
|
+
end
|
106
|
+
|
107
|
+
break unless commit_suitable
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
if suitable_release_sha == last_release_commit
|
112
|
+
puts "No suitable commit to pick for release"
|
113
|
+
else
|
114
|
+
puts "Commit that can be pickup up for release is: #{suitable_release_sha}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
desc "release_plan", "Prepares a release plan"
|
120
|
+
method_option :repository_path, :default => "#{Dir.pwd}", :desc => "Git repository path"
|
121
|
+
method_option :branch, :default => "master", :desc => "Git branch to consider"
|
122
|
+
method_option :last_release_commit, :type => :string, :desc => "Commit id (SHA) of previous release "
|
123
|
+
method_option :target_release_commit, :type => :string, :desc => "Commit id (SHA) of intended release "
|
124
|
+
method_option :html, :type => :boolean, :default => false, :desc => "pretty html output"
|
125
|
+
def release_plan
|
126
|
+
last_release_commit = options[:last_release_commit]
|
127
|
+
target_release_commit = options[:target_release_commit]
|
128
|
+
code_repo = CodeRepository.new(options[:repository_path],last_release_commit)
|
129
|
+
tracker = PivotalTracker.new
|
130
|
+
stories,untagged_commits = tracker.extract_story_ids(code_repo.commits.reverse)
|
131
|
+
|
132
|
+
commits_ready = []
|
133
|
+
stories_included = []
|
134
|
+
|
135
|
+
all_commits = code_repo.commits.reverse
|
136
|
+
commits_ready << all_commits.shift
|
137
|
+
while all_commits.length > 0 && commits_ready.last.oid != target_release_commit
|
138
|
+
commits_ready << all_commits.shift
|
139
|
+
end
|
140
|
+
|
141
|
+
commits_ready.shift #can ignore first commit, its the last release commit
|
142
|
+
stories_ready = []
|
143
|
+
commits_to_cherry_pick = []
|
144
|
+
sha_list =commits_ready.map(&:oid)
|
145
|
+
sha_list.each {|c|
|
146
|
+
s = stories.select {|k,v| v.include?(c)}
|
147
|
+
s.each_pair{|k,v|
|
148
|
+
other_commits = v - [c]
|
149
|
+
other_commits.each {|oc|
|
150
|
+
if !sha_list.include?(oc)
|
151
|
+
commits_to_cherry_pick << oc
|
152
|
+
end
|
153
|
+
}
|
154
|
+
}
|
155
|
+
stories_ready += s.keys
|
156
|
+
}
|
157
|
+
stories_ready.uniq!
|
158
|
+
tracker.load_stories(stories_ready,true)
|
159
|
+
|
160
|
+
#cherry-picks have to be arranged in order which appears in repository
|
161
|
+
ordered_cherry_picks = []
|
162
|
+
all_commits.each {|c|
|
163
|
+
ordered_cherry_picks << c.oid if commits_to_cherry_pick.include?(c.oid)
|
164
|
+
}
|
165
|
+
|
166
|
+
|
167
|
+
md_msg = ""
|
168
|
+
|
169
|
+
md_msg << "Release can be locked at commit #{sha_list.last} \n\n"
|
170
|
+
|
171
|
+
md_msg << "Stories being released are:\n\n"
|
172
|
+
tracker.stories.each {|story|
|
173
|
+
md_msg << " * (#{story[:type]}) [#{story[:name]}](#{story[:url]}) requested by **#{story[:requested_by]}** owned by #{code_repo.contributors(stories[story[:id]]).join(', ')} "
|
174
|
+
md_msg << "(current status:<font color='red'>#{story[:status]}</font>)" if story[:status] != 'accepted'
|
175
|
+
md_msg << " \n"
|
176
|
+
}
|
177
|
+
|
178
|
+
|
179
|
+
md_msg << "\n\nCommits that needs to be cherry-picked as they are part of above stories \n\n"
|
180
|
+
ordered_cherry_picks.reverse.each {|c|
|
181
|
+
c = code_repo.lookup(c)
|
182
|
+
md_msg << " * #{c.oid} #{c.message.gsub("\n"," ")[0...140]} by #{c.author[:name]} \n"
|
183
|
+
}
|
184
|
+
|
185
|
+
puts md_msg
|
186
|
+
|
187
|
+
if options[:html] == true
|
188
|
+
markdown = RDiscount.new(md_msg)
|
189
|
+
temp_html_file = "/tmp/tmp_msg_#{Time.now.to_i}.html"
|
190
|
+
File.open(temp_html_file, 'w') {|f| f.write(markdown.to_html) }
|
191
|
+
system("open #{temp_html_file}")
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
PipelineCmd.start
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "deployment_pipeline"
|
5
|
+
gem.description = "Deployment Pipeline makes Continuous Deployment super easy."
|
6
|
+
gem.homepage = "https://github.com/parolkar/deployment_pipeline"
|
7
|
+
gem.summary = gem.description
|
8
|
+
gem.version = "0.0.0"
|
9
|
+
gem.authors = ["Abhishek Parolkar"]
|
10
|
+
gem.email = "abhishek@parolkar.com"
|
11
|
+
gem.has_rdoc = false
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.add_dependency "rugged","0.16.0"
|
17
|
+
gem.add_dependency "thor","0.14.6"
|
18
|
+
gem.add_dependency "rdiscount"
|
19
|
+
gem.add_dependency "progressbar","0.11.0"
|
20
|
+
gem.add_dependency "hpricot"
|
21
|
+
end
|
22
|
+
|
Binary file
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rugged'
|
2
|
+
|
3
|
+
class CodeRepository
|
4
|
+
attr_reader :commits
|
5
|
+
def initialize(repository_path,from_commit,to_commit = nil)
|
6
|
+
@commits = []
|
7
|
+
@repo = Rugged::Repository.new(repository_path)
|
8
|
+
|
9
|
+
to_commit ||= get_head_ish
|
10
|
+
walker = @repo.walk(to_commit)
|
11
|
+
|
12
|
+
walker.each { |c|
|
13
|
+
@commits << c
|
14
|
+
break if c.oid == from_commit
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def contributors(commit_list)
|
19
|
+
|
20
|
+
return [] if commit_list.empty?
|
21
|
+
author_names = []
|
22
|
+
@commits.each do |c|
|
23
|
+
next unless @commits.map(&:oid).include?(c.oid)
|
24
|
+
author = c.author
|
25
|
+
names = author[:name]
|
26
|
+
names = names.split("&")
|
27
|
+
names.each{|n|
|
28
|
+
n =n.strip
|
29
|
+
author_names << n unless author_names.include?(n)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
author_names
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_head_ish
|
36
|
+
@repo.head.target
|
37
|
+
end
|
38
|
+
|
39
|
+
def lookup(oid)
|
40
|
+
@repo.lookup(oid)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
data/lib/pipeline.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'thor'
|
4
|
+
require 'pg'
|
5
|
+
require 'date'
|
6
|
+
require 'chronic'
|
7
|
+
require 'active_support/all'
|
8
|
+
require "benchmark"
|
9
|
+
require 'net/smtp'
|
10
|
+
require 'progressbar'
|
11
|
+
require 'rdiscount'
|
12
|
+
require 'hpricot'
|
13
|
+
require 'net/http'
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
require File.dirname(__FILE__) + '/code_repository'
|
17
|
+
require File.dirname(__FILE__) + '/tracker'
|
data/lib/tracker.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
|
2
|
+
class Tracker
|
3
|
+
STORY_TAG_PATTERN ||=/\[\w*\s*\#(\d*)\]/ #Default example [fixes #30922794] or [#30972795]
|
4
|
+
attr_reader :stories
|
5
|
+
|
6
|
+
def initialize(tracker_ids,api_key)
|
7
|
+
raise("Missing tracker information") if (@api_key.empty? || @tracker_ids.empty?)
|
8
|
+
@tag_pattern = STORY_TAG_PATTERN
|
9
|
+
@stories = []
|
10
|
+
@story_ids = []
|
11
|
+
end
|
12
|
+
def extract_story_ids(commits_list)
|
13
|
+
raise("Must implement #extract_story_ids in #{self.class.to_s}")
|
14
|
+
end
|
15
|
+
def load_stories(story_id_list = nil)
|
16
|
+
raise("Must implement #load_stories in #{self.class.to_s}")
|
17
|
+
end
|
18
|
+
def story_obj(story_id)
|
19
|
+
raise("Must implement #story_obj in #{self.class.to_s}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class PivotalTracker < Tracker
|
24
|
+
def initialize(tracker_ids = PIVOTAL_TRACKER_PROJECT_IDS,api_key = PIVOTAL_TRACKER_TOKEN)
|
25
|
+
@tracker_ids = tracker_ids
|
26
|
+
@api_key = api_key
|
27
|
+
super(@tracker_ids,@api_key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_story_ids(commits_list)
|
31
|
+
stories = {}
|
32
|
+
untagged_commits = []
|
33
|
+
commits_list.each { |c|
|
34
|
+
stories_related_to_commit = c.message.scan(@tag_pattern)
|
35
|
+
if stories_related_to_commit.empty?
|
36
|
+
untagged_commits << c
|
37
|
+
else
|
38
|
+
|
39
|
+
stories_related_to_commit.flatten.each {|s|
|
40
|
+
@story_ids << s
|
41
|
+
stories[s] ||= []
|
42
|
+
stories[s] << c.oid.to_s
|
43
|
+
}
|
44
|
+
end
|
45
|
+
}
|
46
|
+
return stories,untagged_commits
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_stories(story_id_list = nil,show_progress = false)
|
50
|
+
unless story_id_list.nil?
|
51
|
+
@story_ids = story_id_list
|
52
|
+
end
|
53
|
+
@stories = []
|
54
|
+
if show_progress
|
55
|
+
progressbar = ProgressBar.new("Fetching...", @story_ids.length * @tracker_ids.length)
|
56
|
+
progressbar.bar_mark = '='
|
57
|
+
end
|
58
|
+
@story_ids.each do |story_id|
|
59
|
+
story = story_obj(story_id,progressbar)
|
60
|
+
if story
|
61
|
+
@stories << story
|
62
|
+
else
|
63
|
+
puts "WARNING: Story(#{story_id}) not found in any tracker(#{@tracker_ids.inspect})"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
progressbar.finish if show_progress
|
67
|
+
@stories
|
68
|
+
end
|
69
|
+
|
70
|
+
def story_obj(story_id,progress_bar = nil)
|
71
|
+
@tracker_ids.each { |tracker_id|
|
72
|
+
obj = fetch_story_data("/projects/#{tracker_id}/stories/#{story_id}",@api_key)
|
73
|
+
story_obj = obj.at('story')
|
74
|
+
unless story_obj.nil?
|
75
|
+
story = {
|
76
|
+
:id => story_id,
|
77
|
+
:name => story_obj.at('name').innerHTML,
|
78
|
+
:type => story_obj.at('story_type').innerHTML,
|
79
|
+
:requested_by => story_obj.at('requested_by').innerHTML,
|
80
|
+
:url => story_obj.at('url').innerHTML,
|
81
|
+
:status => story_obj.at('current_state').innerHTML
|
82
|
+
}
|
83
|
+
return story
|
84
|
+
end
|
85
|
+
progress_bar.inc if progress_bar
|
86
|
+
}
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_story_data(obj_path,token)
|
91
|
+
url = "http://www.pivotaltracker.com/services/v3/#{obj_path}"
|
92
|
+
resource_uri = URI.parse(url)
|
93
|
+
response = Net::HTTP.start(resource_uri.host, resource_uri.port) do |http|
|
94
|
+
http.get(resource_uri.path, {'X-TrackerToken' => "#{token}"})
|
95
|
+
end
|
96
|
+
doc = Hpricot(response.body)
|
97
|
+
rescue
|
98
|
+
doc = Hpricot("<xml></xml>")
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# This is a config for Deployment Pipeline Project
|
2
|
+
|
3
|
+
PIVOTAL_TRACKER_TOKEN = "your_cryptic_pivotal_token"
|
4
|
+
PIVOTAL_TRACKER_PROJECT_IDS = ["proj_id1","proj_id2"]
|
5
|
+
|
6
|
+
#ActiveRecord::Base.establish_connection(
|
7
|
+
# :adapter => "postgres",
|
8
|
+
# :host => "localhost",
|
9
|
+
# :database => "appdb",
|
10
|
+
# :username => "appuser",
|
11
|
+
# :password => "secret"
|
12
|
+
#)
|
13
|
+
|
14
|
+
# STORY_TAG_PATTERN=/\[\w*\s*\#(\d*)\]/ #This regexp will extract ids of stories from commit
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CodeRepository do
|
4
|
+
let(:repository_path) {"/some/repo/path"}
|
5
|
+
let(:from_commit) {"shashashashashasahsahsah1"}
|
6
|
+
let(:commit1) {
|
7
|
+
commit = {}
|
8
|
+
stub(commit).oid {"shasha1"}
|
9
|
+
stub(commit).message {"[#1234] strory1"}
|
10
|
+
stub(commit).author {{:name => "Tom Cruise & Rajinikanth"}}
|
11
|
+
commit
|
12
|
+
}
|
13
|
+
before do
|
14
|
+
mock.instance_of(CodeRepository).get_head_ish {"shashasha"}
|
15
|
+
mock(Rugged::Repository).new(repository_path) {|repo|
|
16
|
+
stub(repo).walk { [commit1] }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have commits" do
|
21
|
+
cr = CodeRepository.new(repository_path,from_commit)
|
22
|
+
cr.commits.should include(commit1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should provide list of all contributors" do
|
26
|
+
cr = CodeRepository.new(repository_path,from_commit)
|
27
|
+
commiters = cr.contributors([commit1])
|
28
|
+
commiters.should include("Rajinikanth")
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require "rr"
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/pipeline')
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# Use color in STDOUT
|
7
|
+
config.color_enabled = true
|
8
|
+
|
9
|
+
# Use color not only in STDOUT but also in pagers and files
|
10
|
+
config.tty = true
|
11
|
+
|
12
|
+
# Use the specified formatter
|
13
|
+
config.formatter = :documentation # :progress, :html, :textmate
|
14
|
+
|
15
|
+
config.mock_with :rr
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tracker do
|
4
|
+
|
5
|
+
it "should act like abstract base class to help developers implement more trackers" do
|
6
|
+
|
7
|
+
class FooTracker < Tracker
|
8
|
+
def initialize()
|
9
|
+
@tracker_ids = ['tracker_id']
|
10
|
+
@api_key = 'api_key'
|
11
|
+
super(@tracker_ids,@api_key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
foo_tracker = FooTracker.new()
|
16
|
+
expect {foo_tracker.story_obj("1234")}.to raise_error(RuntimeError, /Must implement #story_obj in FooTracker/)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
describe PivotalTracker do
|
23
|
+
PIVOTAL_TRACKER_PROJECT_IDS = ['test_traker_id']
|
24
|
+
PIVOTAL_TRACKER_TOKEN = "api_key"
|
25
|
+
describe "#story_obj" do
|
26
|
+
it "should fetch story information using pivotal api" do
|
27
|
+
mock(Net::HTTP).start(anything,anything) {|response|
|
28
|
+
stub(response).body {
|
29
|
+
"<story>
|
30
|
+
<name>Story Name</name>
|
31
|
+
<story_type>feature</story_type>
|
32
|
+
<requested_by>Tom</requested_by>
|
33
|
+
<url>/path</url>
|
34
|
+
<current_state>finished</current_state>
|
35
|
+
</story>
|
36
|
+
"
|
37
|
+
}
|
38
|
+
response
|
39
|
+
}
|
40
|
+
pivotal_tracker = PivotalTracker.new()
|
41
|
+
story = pivotal_tracker.story_obj('1234')
|
42
|
+
|
43
|
+
story[:name].should == "Story Name"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: deployment_pipeline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Abhishek Parolkar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rugged
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - '='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.16.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - '='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.16.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: thor
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - '='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.14.6
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - '='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.14.6
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rdiscount
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: progressbar
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.11.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - '='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.11.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: hpricot
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Deployment Pipeline makes Continuous Deployment super easy.
|
95
|
+
email: abhishek@parolkar.com
|
96
|
+
executables:
|
97
|
+
- pipeline
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- Gemfile
|
102
|
+
- Gemfile.lock
|
103
|
+
- README.md
|
104
|
+
- bin/pipeline
|
105
|
+
- deployment_pipeline.gemspec
|
106
|
+
- extra/rugged-0.16.0.gem
|
107
|
+
- lib/code_repository.rb
|
108
|
+
- lib/pipeline.rb
|
109
|
+
- lib/tracker.rb
|
110
|
+
- sample.pipeline_config
|
111
|
+
- spec/code_repository_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
- spec/tracker_spec.rb
|
114
|
+
homepage: https://github.com/parolkar/deployment_pipeline
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.8.24
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Deployment Pipeline makes Continuous Deployment super easy.
|
138
|
+
test_files:
|
139
|
+
- spec/code_repository_spec.rb
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
- spec/tracker_spec.rb
|