├── .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 |
--------------------------------------------------------------------------------