octopolo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +5 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.markdown +55 -0
  10. data/Rakefile +38 -0
  11. data/bash_completion.sh +13 -0
  12. data/bin/octopolo +21 -0
  13. data/bin/op +21 -0
  14. data/lib/octopolo.rb +15 -0
  15. data/lib/octopolo/changelog.rb +27 -0
  16. data/lib/octopolo/cli.rb +210 -0
  17. data/lib/octopolo/commands/accept_pull.rb +8 -0
  18. data/lib/octopolo/commands/compare_release.rb +9 -0
  19. data/lib/octopolo/commands/deployable.rb +8 -0
  20. data/lib/octopolo/commands/github_auth.rb +5 -0
  21. data/lib/octopolo/commands/new_branch.rb +9 -0
  22. data/lib/octopolo/commands/new_deployable.rb +8 -0
  23. data/lib/octopolo/commands/new_staging.rb +8 -0
  24. data/lib/octopolo/commands/octopolo_setup.rb +5 -0
  25. data/lib/octopolo/commands/pivotal_auth.rb +5 -0
  26. data/lib/octopolo/commands/pull_request.rb +13 -0
  27. data/lib/octopolo/commands/signoff.rb +10 -0
  28. data/lib/octopolo/commands/stage_up.rb +8 -0
  29. data/lib/octopolo/commands/stale_branches.rb +11 -0
  30. data/lib/octopolo/commands/sync_branch.rb +11 -0
  31. data/lib/octopolo/commands/tag_release.rb +13 -0
  32. data/lib/octopolo/config.rb +146 -0
  33. data/lib/octopolo/convenience_wrappers.rb +46 -0
  34. data/lib/octopolo/dated_branch_creator.rb +81 -0
  35. data/lib/octopolo/git.rb +262 -0
  36. data/lib/octopolo/github.rb +95 -0
  37. data/lib/octopolo/github/commit.rb +45 -0
  38. data/lib/octopolo/github/pull_request.rb +126 -0
  39. data/lib/octopolo/github/pull_request_creator.rb +127 -0
  40. data/lib/octopolo/github/user.rb +40 -0
  41. data/lib/octopolo/jira/story_commenter.rb +26 -0
  42. data/lib/octopolo/pivotal.rb +44 -0
  43. data/lib/octopolo/pivotal/story_commenter.rb +19 -0
  44. data/lib/octopolo/pull_request_merger.rb +99 -0
  45. data/lib/octopolo/renderer.rb +37 -0
  46. data/lib/octopolo/reports.rb +18 -0
  47. data/lib/octopolo/scripts.rb +23 -0
  48. data/lib/octopolo/scripts/accept_pull.rb +67 -0
  49. data/lib/octopolo/scripts/compare_release.rb +52 -0
  50. data/lib/octopolo/scripts/deployable.rb +27 -0
  51. data/lib/octopolo/scripts/github_auth.rb +87 -0
  52. data/lib/octopolo/scripts/new_branch.rb +34 -0
  53. data/lib/octopolo/scripts/new_deployable.rb +14 -0
  54. data/lib/octopolo/scripts/new_staging.rb +15 -0
  55. data/lib/octopolo/scripts/octopolo_setup.rb +55 -0
  56. data/lib/octopolo/scripts/pivotal_auth.rb +44 -0
  57. data/lib/octopolo/scripts/pull_request.rb +127 -0
  58. data/lib/octopolo/scripts/signoff.rb +85 -0
  59. data/lib/octopolo/scripts/stage_up.rb +26 -0
  60. data/lib/octopolo/scripts/stale_branches.rb +54 -0
  61. data/lib/octopolo/scripts/sync_branch.rb +37 -0
  62. data/lib/octopolo/scripts/tag_release.rb +70 -0
  63. data/lib/octopolo/templates/pull_request_body.erb +24 -0
  64. data/lib/octopolo/user_config.rb +112 -0
  65. data/lib/octopolo/version.rb +3 -0
  66. data/lib/octopolo/week.rb +130 -0
  67. data/octopolo.gemspec +31 -0
  68. data/spec/.DS_Store +0 -0
  69. data/spec/octopolo/cli_spec.rb +310 -0
  70. data/spec/octopolo/config_spec.rb +344 -0
  71. data/spec/octopolo/convenience_wrappers_spec.rb +80 -0
  72. data/spec/octopolo/dated_branch_creator_spec.rb +143 -0
  73. data/spec/octopolo/git_spec.rb +419 -0
  74. data/spec/octopolo/github/commit_spec.rb +59 -0
  75. data/spec/octopolo/github/pull_request_creator_spec.rb +174 -0
  76. data/spec/octopolo/github/pull_request_spec.rb +291 -0
  77. data/spec/octopolo/github/user_spec.rb +65 -0
  78. data/spec/octopolo/github_spec.rb +169 -0
  79. data/spec/octopolo/jira/stor_commenter_spec.rb +30 -0
  80. data/spec/octopolo/pivotal/story_commenter_spec.rb +34 -0
  81. data/spec/octopolo/pivotal_spec.rb +61 -0
  82. data/spec/octopolo/pull_request_merger_spec.rb +144 -0
  83. data/spec/octopolo/renderer_spec.rb +35 -0
  84. data/spec/octopolo/scripts/accept_pull_spec.rb +76 -0
  85. data/spec/octopolo/scripts/compare_release_spec.rb +115 -0
  86. data/spec/octopolo/scripts/deployable_spec.rb +52 -0
  87. data/spec/octopolo/scripts/github_auth_spec.rb +156 -0
  88. data/spec/octopolo/scripts/new_branch_spec.rb +41 -0
  89. data/spec/octopolo/scripts/new_deployable_spec.rb +18 -0
  90. data/spec/octopolo/scripts/new_staging_spec.rb +18 -0
  91. data/spec/octopolo/scripts/octopolo_setup_spec.rb +120 -0
  92. data/spec/octopolo/scripts/pivotal_auth_spec.rb +77 -0
  93. data/spec/octopolo/scripts/pull_request_spec.rb +217 -0
  94. data/spec/octopolo/scripts/signoff_spec.rb +139 -0
  95. data/spec/octopolo/scripts/stage_up_spec.rb +52 -0
  96. data/spec/octopolo/scripts/stale_branches_spec.rb +81 -0
  97. data/spec/octopolo/scripts/sync_branch_spec.rb +57 -0
  98. data/spec/octopolo/scripts/tag_release_spec.rb +108 -0
  99. data/spec/octopolo/user_config_spec.rb +167 -0
  100. data/spec/octopolo_spec.rb +7 -0
  101. data/spec/spec_helper.rb +29 -0
  102. data/spec/support/engine_yard.cache +0 -0
  103. data/spec/support/sample_octopolo.yml +2 -0
  104. data/spec/support/sample_user.yml +2 -0
  105. data/templates/lib.erb +23 -0
  106. data/templates/script.erb +7 -0
  107. data/templates/spec.erb +29 -0
  108. metadata +344 -0
@@ -0,0 +1,44 @@
1
+ require "pivotal-tracker" # this is the gem we're currently using
2
+ require_relative "scripts/pivotal_auth"
3
+
4
+ module Octopolo
5
+ module Pivotal
6
+ # NOTE Should probably extract out to
7
+ class Client
8
+ include UserConfigWrapper
9
+ # Public: Initialize a new instance of Pivotal::Client wrapper class
10
+ def initialize
11
+ # no idea why this is off by default (as of 2013-04-18)
12
+ ::PivotalTracker::Client.use_ssl = true
13
+ begin
14
+ ::PivotalTracker::Client.token = user_config.pivotal_token
15
+ rescue UserConfig::MissingPivotalAuth
16
+ Scripts::PivotalAuth.run
17
+ ::PivotalTracker::Client.token = UserConfig.parse.pivotal_token
18
+ end
19
+ end
20
+
21
+ # Public: Fetch an API token for the given authentication
22
+ #
23
+ # email - a String containing the email address used to log in
24
+ # password - a String containing the password
25
+ #
26
+ # Returns a String or raises BadCredentials
27
+ def self.fetch_token(email, password)
28
+ ::PivotalTracker::Client.token(email, password)
29
+ rescue RestClient::Unauthorized
30
+ raise BadCredentials, "No token received from Pivotal Tracker. Please check your credentials and try again."
31
+ end
32
+
33
+ def find_story(story_id)
34
+ @projects = PivotalTracker::Project.all
35
+ @projects.map{ |project| project.stories.find(story_id) }.compact.first || raise(StoryNotFound, "No Story was found with that ID in your Projects")
36
+ end
37
+ end
38
+
39
+ # the credentials that you've entered are wrong
40
+ BadCredentials = Class.new(StandardError)
41
+ # 404 from the PT api
42
+ StoryNotFound = Class.new(StandardError)
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../pivotal'
2
+
3
+ module Octopolo
4
+ module Pivotal
5
+ class StoryCommenter
6
+ attr_accessor :story
7
+ attr_accessor :comment
8
+
9
+ def initialize(story_id, comment)
10
+ self.story = Pivotal::Client.new.find_story(story_id)
11
+ self.comment = comment
12
+ end
13
+
14
+ def perform
15
+ story.notes.new(:owner => story, :text => comment).create
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,99 @@
1
+ require_relative "scripts"
2
+ require_relative "git"
3
+ require_relative "github/pull_request"
4
+ require_relative "dated_branch_creator"
5
+
6
+ module Octopolo
7
+ class PullRequestMerger
8
+ include ConfigWrapper
9
+ include CLIWrapper
10
+ include GitWrapper
11
+
12
+ attr_accessor :branch_type, :pull_request_id, :options, :pull_request
13
+
14
+ # Public: Initialize a new instance of DatedBranchCreator
15
+ #
16
+ # branch_type - Name of the type of branch (e.g., staging or deployable)
17
+ # pull_request_id - The pull request id to use for the merge
18
+ # options - hash of options
19
+ # - post_comment - whether or not to post a comment on the pull-request
20
+ def initialize(branch_type, pull_request_id, options={})
21
+ self.branch_type = branch_type
22
+ self.pull_request_id = pull_request_id
23
+ self.options = options
24
+ end
25
+
26
+ # Public: Create a new branch of the given type for today's date
27
+ #
28
+ # branch_type - Name of the type of branch (e.g., staging or deployable)
29
+ # post_comment - Whether or not to comment on the pull-request
30
+ #
31
+ # Returns a DatedBranchCreator
32
+ def self.perform(branch_type, pull_request_id, options={})
33
+ new(branch_type, pull_request_id, options).tap do |creator|
34
+ creator.perform
35
+ end
36
+ end
37
+
38
+ # Public: Create the branch and handle related processing
39
+ def perform
40
+ git.if_clean do
41
+ check_out_branch
42
+ merge_pull_request
43
+ comment_about_merge
44
+ end
45
+ rescue GitHub::PullRequest::NotFound
46
+ cli.say "Unable to find pull request #{pull_request_id}. Please retry with a valid ID."
47
+ rescue Git::MergeFailed
48
+ cli.say "Merge failed. Please identify the source of this merge conflict resolve this conflict in your pull request's branch. NOTE: Merge conflicts resolved in the #{branch_type} branch are NOT used when deploying."
49
+ rescue Git::CheckoutFailed
50
+ cli.say "Checkout of #{branch_to_merge_into} failed. Please contact Infrastructure to determine the cause."
51
+ rescue GitHub::PullRequest::CommentFailed
52
+ cli.say "Unable to write comment. Please navigate to #{pull_request.url} and add the comment, '#{comment_body}'"
53
+ end
54
+
55
+ # Public: Check out the branch
56
+ def check_out_branch
57
+ git.check_out branch_to_merge_into
58
+ rescue Git::NoBranchOfType
59
+ cli.say "No #{branch_type} branch available. Creating one now."
60
+ git.check_out DatedBranchCreator.perform(branch_type).branch_name
61
+ end
62
+
63
+ # Public: Merge the pull request's branch into the checked-out branch
64
+ def merge_pull_request
65
+ git.merge pull_request.branch
66
+ end
67
+
68
+ # Public: Comment that the pull request was merged into the branch
69
+ def comment_about_merge
70
+ pull_request.write_comment comment_body
71
+ end
72
+
73
+ # Public: The content of the comment to post when merged
74
+ #
75
+ # Returns a String
76
+ def comment_body
77
+ body = "Merged into #{branch_to_merge_into}."
78
+ if options[:user_notifications]
79
+ body << " /cc #{options[:user_notifications].map {|name| "@#{name}"}.join(' ')}"
80
+ end
81
+ body
82
+ end
83
+
84
+ # Public: Find the pull request
85
+ #
86
+ # Returns a GitHub::PullRequest
87
+ def pull_request
88
+ @pull_request ||= GitHub::PullRequest.new config.github_repo, pull_request_id
89
+ end
90
+
91
+ # Public: Find the branch
92
+ #
93
+ # Returns a String
94
+ def branch_to_merge_into
95
+ @branch_to_merge_into ||= git.latest_branch_for(branch_type)
96
+ end
97
+ end
98
+ end
99
+
@@ -0,0 +1,37 @@
1
+ require "erb"
2
+ require "ostruct"
3
+
4
+ module Octopolo
5
+ class Renderer
6
+ # Constants for the template file names
7
+ PULL_REQUEST_BODY = "pull_request_body"
8
+
9
+ # Public: Render a given ERB template
10
+ #
11
+ # template - A String contianing the name of the ERB template
12
+ # locals - A Hash containing variables to render. The keys must match the variable names in the template
13
+ #
14
+ # Lifted from [Stack Overflow](http://stackoverflow.com/questions/8954706/render-an-erb-template-with-values-from-a-hash)
15
+ #
16
+ # Example
17
+ #
18
+ # render "first_last_name_template", {first: "Bob", last: "Person"}
19
+ # # => "Bob Person"
20
+ #
21
+ # Returns a String containing the rendered template
22
+ def self.render template, locals
23
+ # template_string, safe_mode = 0, "-" to trim whitespace in ERB tags ending -%> (like Rails)
24
+ ERB.new(contents_of(template), 0, "-").result(OpenStruct.new(locals).instance_eval { binding })
25
+ end
26
+
27
+ # Public: The contents of the named template
28
+ def self.contents_of template
29
+ File.read File.join(template_base_path, "#{template}.erb")
30
+ end
31
+
32
+ # Public: Path to the directory containing the templates
33
+ def self.template_base_path
34
+ File.expand_path(File.join(__FILE__, "../templates"))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ require "csv"
2
+
3
+ module Octopolo
4
+ module Reports
5
+ # Public: Write the report data to the given file name
6
+ #
7
+ # data - Array of report data, each element being a line of the CSV
8
+ # filename - The name of the file to write to
9
+ def self.write_csv data, filename
10
+ CSV.open filename, "w" do |file|
11
+ data.each do |line|
12
+ file.puts line
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,23 @@
1
+ require_relative "git"
2
+
3
+ module Octopolo
4
+ module Scripts
5
+ module Base
6
+ def self.included(klass)
7
+ class << klass
8
+ attr_accessor :config
9
+ attr_accessor :cli
10
+ end
11
+
12
+ klass.config = Octopolo.config
13
+ klass.cli = Octopolo::CLI
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # Mostly used for tests
20
+ unless defined?(GLI)
21
+ require 'gli'
22
+ include GLI::App
23
+ end
@@ -0,0 +1,67 @@
1
+ require_relative "../git"
2
+ require_relative "../github"
3
+ require_relative "../github/pull_request"
4
+ require_relative "../scripts"
5
+
6
+ module Octopolo
7
+ module Scripts
8
+ class AcceptPull
9
+ include Base
10
+ include GitWrapper
11
+ include ConfigWrapper
12
+ include CLIWrapper
13
+
14
+ attr_accessor :pull_request_id
15
+
16
+ def self.execute(pull_request_id)
17
+ pull_request_id ||= Integer(cli.prompt "Pull Request ID: ")
18
+ new(pull_request_id).execute
19
+ end
20
+
21
+ def initialize(pull_request_id)
22
+ @pull_request_id = pull_request_id
23
+ end
24
+
25
+ # Public: Perform the script
26
+ def execute
27
+ GitHub.connect do
28
+ pull_request = GitHub::PullRequest.new(config.github_repo, pull_request_id)
29
+ merge pull_request
30
+ update_changelog pull_request
31
+ end
32
+ rescue GitHub::PullRequest::NotFound
33
+ cli.say "Unable to find a pull request #{pull_request_id} for #{config.github_repo}. Please verify."
34
+ end
35
+
36
+ def merge pull_request
37
+ Git.fetch
38
+ if pull_request.mergeable?
39
+ cli.perform "git merge --no-ff origin/#{pull_request.branch} -m \"Merge pull request ##{pull_request_id} from origin/#{pull_request.branch}\""
40
+ else
41
+ cli.say "There is a merge conflict with this branch and #{config.deploy_branch}."
42
+ cli.say "Please update this branch with #{config.deploy_branch} or perform the merge manually and fix any conflicts"
43
+ end
44
+ end
45
+
46
+ def changelog
47
+ @changelog ||= Changelog.new
48
+ end
49
+
50
+ def update_changelog pull_request
51
+ title = pull_request.title
52
+ authors = pull_request.author_names
53
+ commenters = pull_request.commenter_names
54
+ url = pull_request.url
55
+
56
+ changelog.open do |log|
57
+ log.puts "* #{title}"
58
+ log.puts
59
+ log.puts " > #{authors.join(", ")}: #{commenters.join(', ')}: #{url}"
60
+ log.puts
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,52 @@
1
+ require_relative "../scripts"
2
+
3
+ module Octopolo
4
+ module Scripts
5
+ class CompareRelease
6
+ include Base
7
+ include GitWrapper
8
+ include ConfigWrapper
9
+ include CLIWrapper
10
+
11
+ attr_accessor :start
12
+ attr_accessor :stop
13
+
14
+ def self.execute(start, stop)
15
+ new(start, stop).execute
16
+ end
17
+
18
+ def initialize(start=nil, stop=nil)
19
+ @start = start
20
+ @stop = stop
21
+ end
22
+
23
+ def execute
24
+ ask_starting_tag
25
+ ask_stopping_tag
26
+ open_compare_page
27
+ end
28
+
29
+ # Public: Ask, if not already set, which tag to start with
30
+ def ask_starting_tag
31
+ self.start ||= cli.ask("Start with which tag?", git.recent_release_tags)
32
+ end
33
+
34
+ # Public: Ask, if not already set, which tag to end with
35
+ def ask_stopping_tag
36
+ self.stop ||= cli.ask("Compare from #{start} to which tag?", git.recent_release_tags)
37
+ end
38
+
39
+ # Public: Open the GitHub compare URL for the starting and ending branches
40
+ def open_compare_page
41
+ cli.copy_to_clipboard compare_url
42
+ cli.open compare_url
43
+ end
44
+
45
+ # Public: The GitHub compare URL for the selected tags
46
+ def compare_url
47
+ "https://github.com/#{config.github_repo}/compare/#{start}...#{stop}?w=1"
48
+ end
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,27 @@
1
+ require_relative "../scripts"
2
+ require_relative "../pull_request_merger"
3
+
4
+ module Octopolo
5
+ module Scripts
6
+ class Deployable
7
+ include CLIWrapper
8
+ include ConfigWrapper
9
+
10
+ attr_accessor :pull_request_id
11
+
12
+ def self.execute(pull_request_id=nil)
13
+ new(pull_request_id).execute
14
+ end
15
+
16
+ def initialize(pull_request_id=nil)
17
+ @pull_request_id = pull_request_id
18
+ end
19
+
20
+ # Public: Perform the script
21
+ def execute
22
+ self.pull_request_id ||= cli.prompt("Pull Request ID: ")
23
+ PullRequestMerger.perform Git::DEPLOYABLE_PREFIX, Integer(@pull_request_id), :user_notifications => config.user_notifications
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,87 @@
1
+ require "json"
2
+ require_relative "../github"
3
+ require_relative "../scripts"
4
+
5
+ module Octopolo
6
+ module Scripts
7
+ class GithubAuth
8
+ include CLIWrapper
9
+ include UserConfigWrapper
10
+
11
+ attr_accessor :username
12
+ attr_accessor :password
13
+ attr_accessor :auth_response
14
+ attr_accessor :user_defined_token
15
+
16
+ def execute
17
+ case ask_auth_method
18
+ when "Generate an API token with my credentials"
19
+ ask_credentials
20
+ request_token
21
+ when "I'll enter an access token manually"
22
+ ask_token
23
+ verify_token
24
+ end
25
+ store_token
26
+ rescue GitHub::BadCredentials => e
27
+ cli.say e.message
28
+ end
29
+
30
+ # Private: Give option to manually get token from GitHub and use it instead of using creds (For people that use 2FA)
31
+ def ask_auth_method
32
+ question = "Would you like to generate an GitHub API token with your credentials or enter one manually?\n"\
33
+ "You *must* enter a token manually if you are using GitHub's Two Factor Authentication.\n"\
34
+ "For more information, see https://help.github.com/articles/creating-an-access-token-for-command-line-use\n\n"
35
+ choices = ["Generate an API token with my credentials", "I'll enter an access token manually"]
36
+ selected = cli.ask(question, choices)
37
+ end
38
+ private :ask_auth_method
39
+
40
+ # Private: Request the user's GitHub username and password
41
+ def ask_credentials
42
+ self.username = cli.prompt "Your GitHub username: "
43
+ self.password = cli.prompt_secret "Your GitHub password (never stored): "
44
+ end
45
+ private :ask_credentials
46
+
47
+ # Private: Request an auth token from GitHub
48
+ def request_token
49
+ json = cli.perform_quietly %Q(curl -u '#{username}:#{password}' -d '{"scopes": ["repo"], "notes": "Octopolo"}' https://api.github.com/authorizations)
50
+ self.auth_response = JSON.parse json
51
+ end
52
+ private :request_token
53
+
54
+ # Private: Verify a user_defined_token with GitHub
55
+ def verify_token
56
+ json = cli.perform_quietly %Q(curl -u #{user_defined_token}:x-oauth-basic https://api.github.com/user)
57
+ self.auth_response = JSON.parse json
58
+ end
59
+ private :verify_token
60
+
61
+ # Private: Request the user to give a token to be set manually (required for 2FA)
62
+ def ask_token
63
+ self.username = cli.prompt "Your GitHub username: "
64
+ self.user_defined_token = cli.prompt_secret "Your GitHub API token: "
65
+ end
66
+ private :ask_token
67
+
68
+ # Private: Store the token recieved from GitHub
69
+ #
70
+ # If a token is present in the response, store it in the user config.
71
+ # Otherwise indicate that the authorization did not succeed.
72
+ def store_token
73
+ token = auth_response["login"] ? user_defined_token : auth_response["token"]
74
+ if token
75
+ user_config.set :github_user, username
76
+ user_config.set :github_token, token
77
+ cli.say "Successfully stored GitHub API token."
78
+ else
79
+ raise GitHub::BadCredentials, "Uh oh, your access token couldn't be generated/verified. Please check your credentials and try again."
80
+ end
81
+ end
82
+ private :store_token
83
+
84
+ end
85
+ end
86
+ end
87
+