stories_sync 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +37 -0
- data/bin/stories +9 -0
- data/lib/pivotal.rb +17 -0
- data/lib/stories_sync.rb +177 -0
- data/stories_sync.gemspec +13 -0
- metadata +61 -0
data/README.markdown
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
Stories Sync
|
2
|
+
============
|
3
|
+
|
4
|
+
The idea behind this gem is to have a tool to help you manage your user stories.
|
5
|
+
|
6
|
+
It'a a great combo to use [stories](github.com/citrusbyte/storires) + [Pivotal Tracker](http://www.pivotaltracker.com) for your integration testing, and it works pretty fine out of the box. However, one can't avoid thinking that there's something missing. For example there's no obvious way to run your user stories or generate reports easily; one would have to create a rake task or something like that.
|
7
|
+
|
8
|
+
And what about the ability to have pending stories? seems like a cool idea, but for that to make any sense, one would have to copy/past every story (one by one) on your backlog at the beginning of each sprint to your local stories file, mark them as pending stories and start them one at a time. Seems like a lot of work. It is.
|
9
|
+
|
10
|
+
Finally, if you discover that a story actually needs to be splitted in two, or you just want to add a new story and start implementing it, it's kind of a drag to have to log in to pivotal, create the story, move it to the backlog, mark it as "started", then go back to your local file, copy it there and finally start coding.
|
11
|
+
|
12
|
+
What we wanted is the following workflow:
|
13
|
+
|
14
|
+
$ stories sync
|
15
|
+
|
16
|
+
That's it. That simple command will fetch the pivotal stories and add them as pending stories in your file and also upload any new local story to pivotal.
|
17
|
+
|
18
|
+
If you want to run your stories with the standard stories output:
|
19
|
+
|
20
|
+
$ stories run
|
21
|
+
|
22
|
+
To generate a pdf report:
|
23
|
+
|
24
|
+
$ stories report
|
25
|
+
|
26
|
+
|
27
|
+
Setup and usage
|
28
|
+
---------------
|
29
|
+
|
30
|
+
This gem requires you to have a pivotal.yml in your config directory, but this can be generated automatically. Just execute the following:
|
31
|
+
|
32
|
+
$ stories setup
|
33
|
+
|
34
|
+
This will ask you for your pivotal username and password, then it will fetch your api token and finally it will create a config file for you.
|
35
|
+
After that you are all set. You just need to have a stories file in /test/stories/stories.rb.
|
36
|
+
|
37
|
+
_Right now this gem supports a single stories file only. This is the first version though, we are working for multiple file support._
|
data/bin/stories
ADDED
data/lib/pivotal.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
CONFIG_FILE = File.join("#{Dir.pwd}", "config", "pivotal.yml")
|
2
|
+
|
3
|
+
# Failsafe in case there's no config file
|
4
|
+
raise "You don't have a configuration file yet, try running 'stories setup'" unless File.exists?(CONFIG_FILE)
|
5
|
+
|
6
|
+
PIVOTAL_CONFIG = YAML.load_file(CONFIG_FILE)
|
7
|
+
|
8
|
+
# Interaction with Pivotal
|
9
|
+
class Iteration < ActiveResource::Base
|
10
|
+
self.site = "http://www.pivotaltracker.com/services/v2/projects/#{PIVOTAL_CONFIG[:project][:id]}"
|
11
|
+
headers['X-TrackerToken'] = PIVOTAL_CONFIG[:user][:token]
|
12
|
+
end
|
13
|
+
|
14
|
+
class Story < ActiveResource::Base
|
15
|
+
self.site = "http://www.pivotaltracker.com/services/v2/projects/#{PIVOTAL_CONFIG[:project][:id]}"
|
16
|
+
headers['X-TrackerToken'] = PIVOTAL_CONFIG[:user][:token]
|
17
|
+
end
|
data/lib/stories_sync.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require 'restclient'
|
5
|
+
require "activeresource"
|
6
|
+
require "thor"
|
7
|
+
require "yaml"
|
8
|
+
require File.join("#{File.dirname(__FILE__)}", "pivotal.rb")
|
9
|
+
|
10
|
+
# FIXME We are using both restclient and ARes at the moment.
|
11
|
+
# We should migrate everything to restclient, since it's way smaller.
|
12
|
+
|
13
|
+
class Stories < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
|
16
|
+
desc "add_pending_stories", "Fetch stories and add them to the stories file"
|
17
|
+
def add_pending_stories
|
18
|
+
say_status(:fetch, "Fetching stories from Pivotal.")
|
19
|
+
new_external_stories = difference(fetch_stories, local_stories)
|
20
|
+
|
21
|
+
if new_external_stories.empty?
|
22
|
+
say_status(:complete, "Pivotal stories up to date.")
|
23
|
+
else
|
24
|
+
new_external_stories.each_pair do |label, stories|
|
25
|
+
say_status(:compare, "There are #{stories.size} new stories in Pivotal with the label '#{label}'.")
|
26
|
+
end
|
27
|
+
add_new_stories(new_external_stories)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "upload_new_stories", "Upload new stories to pivotal"
|
32
|
+
def upload_new_stories
|
33
|
+
new_local_stories = difference(local_stories, fetch_stories)
|
34
|
+
|
35
|
+
new_local_stories.each_pair do |label, stories|
|
36
|
+
say_status(:compare, "You have #{stories.size} new local stories in #{label}_test.rb.")
|
37
|
+
end
|
38
|
+
|
39
|
+
if new_local_stories.empty?
|
40
|
+
say_status(:completed, "Your stories up to date.")
|
41
|
+
else
|
42
|
+
new_local_stories.each_pair do |label, stories|
|
43
|
+
stories.each do |story|
|
44
|
+
upload_new_story(story, label)
|
45
|
+
say_status(:append, "Added story: '#{story}'")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
say_status(:completed, "Stories added to Pivotal successfully.")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "sync", "Synchronize your local stories with your Pivotal stories"
|
53
|
+
def sync
|
54
|
+
add_pending_stories
|
55
|
+
upload_new_stories
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "setup", "Fetch your Pivotal token and creates or updates your config file"
|
59
|
+
def setup
|
60
|
+
unless user_config
|
61
|
+
say "You need to create a configuration file"
|
62
|
+
create_config_file
|
63
|
+
else
|
64
|
+
say "This is your pivotal configuration"
|
65
|
+
say user_config
|
66
|
+
create_config_file if yes? "Do you want to change your configuration? (Y/N)"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def difference(hash_a ,hash_b)
|
72
|
+
new_stories = {}
|
73
|
+
hash_a.each_pair do |label, stories|
|
74
|
+
new_stories[label] = stories - hash_b[label].to_a
|
75
|
+
end
|
76
|
+
new_stories
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns {"label_1" => [story_1, story_2, ..], "label_2" => [story_1, story_2, ..]}
|
80
|
+
def fetch_stories
|
81
|
+
Iteration.find(:last).stories.inject({}) do |result, story|
|
82
|
+
result[story.labels] ||= []
|
83
|
+
result[story.labels] << story.name
|
84
|
+
result
|
85
|
+
end
|
86
|
+
rescue NoMethodError; []
|
87
|
+
end
|
88
|
+
|
89
|
+
def fetch_token(user = PIVOTAL_CONFIG[:user][:name], pass = PIVOTAL_CONFIG[:user][:pass])
|
90
|
+
token = RestClient::Resource.new("https://www.pivotaltracker.com/services/tokens/active/guid",
|
91
|
+
:user => user,
|
92
|
+
:password => pass).get
|
93
|
+
token.match(/<guid>(.*)<.guid>/)[1]
|
94
|
+
rescue RestClient::Unauthorized
|
95
|
+
say_status :unauthorized, "Your credentials are invalid, You'll have to try again", :red
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_config_file
|
99
|
+
say_status :setup, "Gathering your Pivotal information"
|
100
|
+
user = ask "What is your user name?"
|
101
|
+
pass = ask "What is your password?"
|
102
|
+
say_status :fetch, "Fetching your Pivotal token"
|
103
|
+
token = fetch_token(user, pass)
|
104
|
+
return if token.nil?
|
105
|
+
project_id = ask "What is your pivotal project id?"
|
106
|
+
create_yaml(user, pass, token, project_id)
|
107
|
+
say_status :create, "./config/pivotal.yml"
|
108
|
+
end
|
109
|
+
|
110
|
+
def user_config
|
111
|
+
File.read(CONFIG_FILE) if File.exists?(CONFIG_FILE)
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO we should consider making this a public method
|
115
|
+
# and merging the extra options if given.
|
116
|
+
def upload_new_story(name, key)
|
117
|
+
# Other options are: requested_by, description, owned_by, labels
|
118
|
+
story = Story.create(:name => name, :estimate => 1, :labels => key)
|
119
|
+
story.current_state = "unstarted"
|
120
|
+
story.save
|
121
|
+
end
|
122
|
+
|
123
|
+
def story_file(label)
|
124
|
+
File.join("#{Dir.pwd}", "test", "stories", "#{label}_test.rb")
|
125
|
+
end
|
126
|
+
|
127
|
+
def files
|
128
|
+
Dir.entries(File.join("#{Dir.pwd}", "test", "stories")).delete_if do |name|
|
129
|
+
name.match(/_test.rb/).nil?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def labels
|
134
|
+
files.inject([]) do |labels, name|
|
135
|
+
labels << name.scan(/(.*)_test.rb/).to_s
|
136
|
+
labels
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def local_stories
|
141
|
+
labels.inject({}) do |result, label|
|
142
|
+
result[label] ||= []
|
143
|
+
result[label] = File.read(story_file(label)).scan(/\n\s*story[\s\n]*(?:"([^"]*)"|'([^']*)')/m).map(&:compact).flatten
|
144
|
+
result
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_new_stories(new_stories)
|
149
|
+
new_stories.each_pair do |label, stories|
|
150
|
+
file = story_file(label)
|
151
|
+
if File.exists?(file)
|
152
|
+
original_file = File.read(file).scan(/(.*)end/m)
|
153
|
+
original_file << "\n # Pending stories\n"
|
154
|
+
|
155
|
+
File.open(story_file(label), "w") do |f|
|
156
|
+
f.puts original_file
|
157
|
+
f.puts stories.map {|story| " story \"#{story}\""}.join("\n")
|
158
|
+
f.puts "end"
|
159
|
+
end
|
160
|
+
else
|
161
|
+
say_status(:warning, "The file '#{file}' doesn't exist", :yellow)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
say_status(:append, "Pending stories added successfully.")
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_yaml(user, pass, token, project_id)
|
168
|
+
config = { :user => { :name => user,
|
169
|
+
:pass => pass,
|
170
|
+
:token => token.to_s },
|
171
|
+
:project => { :id => project_id } }
|
172
|
+
|
173
|
+
File.open(CONFIG_FILE, "w+") do |f|
|
174
|
+
f.puts config.to_yaml
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "stories_sync"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.summary = "Stories Sync"
|
5
|
+
s.description = "Manage and syncrhonize your UATs with Pivotal Tracker."
|
6
|
+
s.authors = ["Evelin Garcia", "Lucas Nasif"]
|
7
|
+
s.email = ["ehqhvm@gmail.com"]
|
8
|
+
|
9
|
+
s.rubyforge_project = "stories_sync"
|
10
|
+
|
11
|
+
s.files = ["README.markdown", "lib/pivotal.rb", "lib/stories_sync.rb", "bin/stories", "stories_sync.gemspec"]
|
12
|
+
end
|
13
|
+
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stories_sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Evelin Garcia
|
8
|
+
- Lucas Nasif
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-10-12 00:00:00 -03:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Manage and syncrhonize your UATs with Pivotal Tracker.
|
18
|
+
email:
|
19
|
+
- ehqhvm@gmail.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files: []
|
25
|
+
|
26
|
+
files:
|
27
|
+
- README.markdown
|
28
|
+
- lib/pivotal.rb
|
29
|
+
- lib/stories_sync.rb
|
30
|
+
- bin/stories
|
31
|
+
- stories_sync.gemspec
|
32
|
+
has_rdoc: true
|
33
|
+
homepage:
|
34
|
+
licenses: []
|
35
|
+
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project: stories_sync
|
56
|
+
rubygems_version: 1.3.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Stories Sync
|
60
|
+
test_files: []
|
61
|
+
|