deployment_pipeline 0.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.
- 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
|