├── .gitignore ├── README ├── config.ru ├── test.rb └── tracker_github_hook.rb /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | IMPORTANT UPDATE: This is no longer supported/maintained, and will most surely stop 2 | working on Jan 27, 2012 when Pivotal deprecates the Tracker API's prior to v3. Also, 3 | the standard GitHub-Pivotal Tracker service hook now handles everything this service 4 | did, and in fact works better now (better handles branch commits and avoiding duplicate 5 | comments in tracker due to merging branches). 6 | 7 | 8 | This app is a small server to serve as a GitHub Post-Receive hook to add 9 | comments, and update state in Pivotal Tracker, similar to say the Lighthouse 10 | service integration. 11 | 12 | As of January 23, 2010, Pivotal's Tracker API itself will have some support for 13 | doing this. See: http://pivotallabs.com/users/dan/blog/articles/1135-pivotal-tracker-api-new-version-v3-to-be-released-on-jan-23 14 | However, note that, as of this writing, they only support a single API token, 15 | which means that all commits will be attributed to the Tracker member whose 16 | token is being used in the post-commit hook. This project solves that by 17 | associating Tracker API tokens with GitHub users (so even if your GitHub and 18 | Tracker emails are different that's ok), and thus you have properly attributed 19 | comments in your Tracker stories when making a commit. 20 | 21 | Also, many thanks to Alan Pinstein (apinstein) for his many contributions. 22 | 23 | Configure your Tracker API key, and Project ID in a config.yml file placed in 24 | the same directory as this app. It should look something like: 25 | 26 | tracker_github_hook: 27 | github_url: 'http://github.com/chris/tracker_github_hook' 28 | tracker_api_token: a1230e72340e3babc96d5e2fab67c18d 29 | tracker_project_id: 123 30 | ref: refs/heads/master 31 | user_api_tokens: 32 | chris: 33 | email: chris@cobaltedge.com 34 | tracker_api_token: a1b2c3d4e5f67890 35 | alan: 36 | email: alan@example.com 37 | tracker_api_token: 0987654321abcdef 38 | 39 | 40 | The label ('tracker_github_hook' in this case) is arbitrary and not used, it's 41 | just their to be a useful bit of info to humans/organize the nested settings, 42 | and is not used. This setup allows you to have one service that supports 43 | multiple Tracker/GitHub projects, just define one of the above blocks for each 44 | one, and then anytime GitHub sends a push, the service will tease out which 45 | GitHub repo it came from and correlate that to which Tracker project you've 46 | assigned to that. 47 | 48 | The "ref" field is also optional; it will tell the hook to ignore commits on 49 | any branches other than the one listed. This is useful to prevent duplication 50 | of comments being pushed into Tracker if you have multiple remote branches. 51 | 52 | The "user_api_tokens" is optional - the primary "tracker_api_token" will be used 53 | by default. But, if you do supply this block, then the hook will correlate the 54 | email address of the author of the GitHub commit, to that within this list, and 55 | if it finds a match, will use the specified Tracker API token. This makes it so 56 | that the comment in your Tracker story shows up as being made by the same person 57 | making the GitHub commit (instead of whoever owns the default API token). 58 | 59 | When you make commits to Git/GitHub, and want a comment and optionally a state 60 | update made to a story in Tracker, add the following text to your commit 61 | message: 62 | 63 | [Story#####] 64 | 65 | or 66 | 67 | [Story##### state:finished] 68 | 69 | where ##### is the story number (see the bottom of an expanded story in 70 | Tracker for its ID). 71 | 72 | A commit message can have more than one [Story####] block, but the entire 73 | commit message will be added to both stories. The duplication is unfortunate 74 | but better than the alternative of ignoring additional story references 75 | altogether. 76 | 77 | This project also requires the following rubygems: 78 | - sinatra 79 | - rest-client 80 | - json 81 | 82 | 83 | More information for, and thanks to: 84 | 85 | Pivotal Tracker API: http://www.pivotaltracker.com/help/api 86 | GitHub Post-Receive Hooks: http://github.com/guides/post-receive-hooks 87 | Sinatra: http://sinatra.rubyforge.org/ 88 | RestClient: http://rubyforge.org/projects/rest-client/ 89 | 90 | 91 | TODO: 92 | - cleanup, better testing 93 | - support other story changes, like assigned user, etc. 94 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'tracker_github_hook' 2 | 3 | ## There is no need to set directories here anymore; 4 | ## Just run the application 5 | 6 | run Sinatra::Application 7 | 8 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # Test our Sinatra Tracker Post-receive hook 3 | # 4 | require 'net/http' 5 | require 'rubygems' 6 | 7 | payload = <<-eos 8 | payload={ 9 | "before": "5aef35982fb2d34e9d9d4502f6ede1072793222d", 10 | "repository": { 11 | "url": "http://github.com/chris/tracker_github_hook", 12 | "name": "github", 13 | "description": "You're lookin' at it.", 14 | "watchers": 5, 15 | "forks": 2, 16 | "private": 1, 17 | "owner": { 18 | "email": "chris@cobaltedge.com", 19 | "name": "chris" 20 | } 21 | }, 22 | "commits": [ 23 | { 24 | "id": "41a212ee83ca127e3c8cf465891ab7216a705f59", 25 | "url": "http://github.com/defunkt/github/commit/41a212ee83ca127e3c8cf465891ab7216a705f59", 26 | "author": { 27 | "email": "chris@cobaltedge.com", 28 | "name": "Chris Bailey" 29 | }, 30 | "message": "This one is a comment only 10 [Story294825] and [Story1234] 2nd line commit", 31 | "timestamp": "2008-02-15T14:57:17-08:00", 32 | "added": ["filepath.rb"] 33 | }, 34 | { 35 | "id": "41a212ee83ca321e3c8cf465891cb7216a705f59", 36 | "url": "http://github.com/defunkt/github/commit/41a212ee83ca127e3c8cf465891ab7216a705f59", 37 | "author": { 38 | "email": "chris@cobaltedge.com", 39 | "name": "Chris Bailey" 40 | }, 41 | "message": "This one does not have a story association", 42 | "timestamp": "2008-02-15T14:58:17-08:00", 43 | "added": ["filepath.rb"] 44 | }, 45 | { 46 | "id": "de8251ff97ee194a289832576287d6f8ad74e3d0", 47 | "url": "http://github.com/defunkt/github/commit/de8251ff97ee194a289832576287d6f8ad74e3d0", 48 | "author": { 49 | "email": "chris@cobaltedge.com", 50 | "name": "Chris Bailey" 51 | }, 52 | "message": "comment and state change 10 [Story294825 state:finished]", 53 | "timestamp": "2008-02-15T14:36:34-08:00" 54 | } 55 | ], 56 | "after": "de8251ff97ee194a289832576287d6f8ad74e3d0", 57 | "ref": "refs/heads/master" 58 | } 59 | eos 60 | 61 | headers = { 'Content-Type' => 'application/x-www-form-urlencoded' } 62 | 63 | http = Net::HTTP.new('localhost', 4567) 64 | resp, data = http.post('/', payload, headers) 65 | 66 | puts "Response code: #{resp.code}" 67 | puts "Response body: #{data}" 68 | -------------------------------------------------------------------------------- /tracker_github_hook.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # GitHub Post-Receive hook handler to add comments, and update state in Pivotal Tracker 4 | # Configure your Tracker API key, and Project ID in a config.yml file placed in the 5 | # same directory as this app. 6 | # When you make commits to Git/GitHub, and want a comment and optionally a state update 7 | # made to Tracker, add the following syntax to your commit message: 8 | # 9 | # [Story#####] 10 | # or 11 | # [Story##### state:finished] 12 | # 13 | 14 | require 'rubygems' 15 | require 'sinatra' 16 | require 'json' 17 | require 'rest_client' 18 | require 'yaml' 19 | 20 | 21 | # load up configuration from YAML file 22 | configure do 23 | begin 24 | config = open(File.expand_path(File.dirname(__FILE__) + '/config.yml')) { |f| YAML.load(f) } 25 | 26 | PROJECTS = Hash.new 27 | config.each do |project| 28 | raise "required configuration settings not found" unless project[1]['tracker_api_token'] && project[1]['tracker_project_id'] 29 | api_tokens = Hash.new 30 | if project[1]['user_api_tokens'] 31 | project[1]['user_api_tokens'].each_value do |user_info| 32 | api_tokens[user_info['email'].downcase] = user_info['tracker_api_token'] 33 | end 34 | end 35 | PROJECTS[project[1]['github_url']] = { :api_token => project[1]['tracker_api_token'], 36 | :project_id => project[1]['tracker_project_id'], 37 | :ref => project[1]['ref'], 38 | :user_api_tokens => api_tokens } 39 | end 40 | rescue => e 41 | puts "Failed to startup: #{e.message}" 42 | puts "Ensure you have a config.yml in this directory with the'tracker_api_token' and 'tracker_project_id' keys/values set." 43 | exit(-1) 44 | end 45 | end 46 | 47 | # The handler for the GitHub post-receive hook 48 | post '/' do 49 | @num_commits = 0 50 | push = JSON.parse(params[:payload]) 51 | tracker_info = PROJECTS[push['repository']['url']] 52 | raise "GitHub Webook triggerd for repo: #{push['repository']['url']}; no matching github_url in config.yml" if tracker_info == nil 53 | if tracker_info[:ref] && push['ref'] != tracker_info[:ref] 54 | puts "Skipping commit for non-tracked ref #{push['ref']}" 55 | end 56 | push['commits'].each { |commit| process_commit(tracker_info, commit) } 57 | "Processed #{@num_commits} commits for stories" 58 | end 59 | 60 | get '/' do 61 | "Have your github webhook point here; bridge works automatically via POST" 62 | end 63 | 64 | 65 | helpers do 66 | def process_commit(tracker_info, commit) 67 | # get commit message 68 | message = commit['message'] 69 | 70 | # get API token for the user who made the commit, if possible 71 | api_token = api_token_for_user(tracker_info, commit['author']['email']) 72 | 73 | # see if there is a Tracker story trigger, and if so, get story ID 74 | message.scan(/\[Story(\d+)([^\]]*)\]/) do |tracker_trigger| 75 | @num_commits += 1 76 | story_id = tracker_trigger[0] 77 | 78 | # post comment to the story 79 | RestClient.post(create_api_url(tracker_info[:project_id], story_id, '/notes'), 80 | "(from [#{commit['id']}]) #{message}", 81 | tracker_api_headers(api_token)) 82 | 83 | # See if we have a state change 84 | state = tracker_trigger[1].match(/.*state:(\s?\w+).*/) 85 | if state 86 | state = state[1].strip 87 | 88 | RestClient.put(create_api_url(tracker_info[:project_id], story_id), 89 | "#{state}", 90 | tracker_api_headers(api_token)) 91 | end 92 | end 93 | end 94 | 95 | def api_token_for_user(tracker_info, email) 96 | tracker_info[:user_api_tokens][email.downcase] || tracker_info[:api_token] 97 | end 98 | 99 | def create_api_url(project_id, story_id, extra_path_elemets='') 100 | "http://www.pivotaltracker.com/services/v2/projects/#{project_id}/stories/#{story_id}#{extra_path_elemets}" 101 | end 102 | 103 | def tracker_api_headers(api_token) 104 | { 'X-TrackerToken' => api_token, 'Content-type' => 'application/xml' } 105 | end 106 | 107 | end 108 | --------------------------------------------------------------------------------