Skip to content
This repository has been archived by the owner on Jan 31, 2019. It is now read-only.

Commit

Permalink
Merge branch 'master' of https://github.com/Rallydev/github-services
Browse files Browse the repository at this point in the history
…into Rallydev-master
  • Loading branch information
technoweenie committed Apr 11, 2012
2 parents 93ae312 + 6d9d783 commit f518a17
Show file tree
Hide file tree
Showing 3 changed files with 419 additions and 0 deletions.
59 changes: 59 additions & 0 deletions docs/rally
@@ -0,0 +1,59 @@
Rally
=====

Rally (http://www.rallydev.com) is the recognized leader in Agile
application lifecycle management (ALM). Rally is dedicated to helping
organizations embrace Agile and Lean development and portfolio management
practices that dramatically increase the pace of innovation, improve
product quality and boost customer satisfaction.

The GitHub-Services integration for Rally creates Changeset/Change
information in Rally associated with the Workspace and SCMRepository
of your choice. In Rally, when looking at an artifact, you can see
detail on Changesets associated with commits from GitHub push activities.
Rally also provides reports that use the Artifact, Changeset and Change
information to provide insight into files that get changed frequently
or that are associated with higher than normal defect rates and other
useful metrics based reports.

The integration scans for tokens in the commit message that conform to a
Rally Artifact FormattedID format. These tokens are validated against
Rally so that a reference to a valid Rally Artifact (UserStory, Defect,
TestCase, Task, etc.) results in the association of the Artifact to the
Changeset posted in Rally performed by this integration.
Commits containing a commit message that doesn't have a valid artifact
reference also get posted to Rally so that they show up in any Rally
reports against Changesets/Changes.

NB: Uri fields in Rally Changeset/Change records refer the to master branch.

Install Notes
-------------

You'll need a Rally account (see http://rally.rallydev.com).

1. server - (REQUIRED) is the hostname of the Rally server.
You do not have to provide the domain name portion of the server
if the name of the server is a Rally SaaS server,
ie., rally1 or trial or demo.
If you are utilizing the Rally OnPrem product, you'll need
to provide fully qualified hostname.
2. username - (REQUIRED) is the Rally UserName for the subscription.
Make sure you specify the UserName (not the DisplayName or FirstName and LastName or Email).
3. password - (REQUIRED) is the password associated with the UserName for the Rally Subscription.
4. workspace - (REQUIRED) is the name of the Rally Workspace in which the target SCMRepository
resides.
5. repository - (OPTIONAL) is the name of the Rally SCMRepository. By default, if not provided,
this integration will use the name of your GitHub repository as the SCMRepository name.
But, you have the ability to name the Rally SCMRepository to your choice of a name.


Developer Notes
----------------

data
- server (Rally server name (domain info optional))
- username (Rally subscription UserName)
- password (Rally subscription Password )
- workspace (name of Rally workspace holding target repository)
- repository (name of Rally SCMRepository (defaults to GitHub repository name))
165 changes: 165 additions & 0 deletions services/rally.rb
@@ -0,0 +1,165 @@
require 'time'

class Service::Rally < Service
string :server, :username, :workspace, :repository
password :password

attr_accessor :wksp_ref, :repo_ref, :user_cache, :chgset_uri
attr_reader :art_type, :branch, :artifact_detector

def receive_push
server = data['server']
username = data['username']
password = data['password']
workspace = data['workspace']
@branch = payload['ref'].split('/')[-1] # most of the time it'll be refs/heads/master ==> master
repo = payload['repository']['name']
repo_owner = payload['repository']['owner']['name']
@chgset_uri = 'https://github.com/%s/%s' % [repo_owner, repo]

http.ssl[:verify] = false
if server =~ /^https?:\/\// # if they have http:// or https://, leave server value unchanged
http.url_prefix = "#{server}/slm/webservice/1.29"
else
server = "#{server}.rallydev.com" if server !~ /\./ # leave unchanged if '.' in server
http.url_prefix = "https://#{server}/slm/webservice/1.29"
end
http.basic_auth(username, password)
http.headers['Content-Type'] = 'application/json'
http.headers['X-RallyIntegrationVendor'] = 'Rally'
http.headers['X-RallyIntegrationName'] = 'GitHub-Service'
http.headers['X-RallyIntegrationVersion'] = '1.0'

@wksp_ref = validateWorkspace(workspace)
@repo_ref = getOrCreateRepo(repo, repo_owner)

@art_type = { 'D' => 'defect', 'DE' => 'defect', 'DS' => 'defectsuite',
'TA' => 'task', 'TC' => 'testcase',
'S' => 'hierarchicalrequirement',
'US' => 'hierarchicalrequirement'
}
formatted_id_pattern = '^(%s)\d+[\.:;]?$' % @art_type.keys.join('|') # '^(D|DE|DS|TA|TC|S|US)\d+[\.:;]?$'
@artifact_detector = Regexp.compile(formatted_id_pattern)

@user_cache = {}
payload['commits'].each do |commit|
artifact_refs = snarfArtifacts(commit['message'])
addChangeset(commit, artifact_refs)
end
end

def addChangeset(commit, artifact_refs)
author = commit['author']['email']
if !@user_cache.has_key?(author)
user = rallyQuery('User', 'Name,UserName', 'UserName = "%s"' % [author])
user_ref = ""
user_ref = itemRef(user) unless user.nil?
@user_cache[author] = user_ref
end
user_ref = @user_cache[author]
message = commit['message']
message = message[0..3999] unless message.size <= 4000
changeset = { 'SCMRepository' => @repo_ref,
'Revision' => commit['id'],
'Author' => user_ref,
'CommitTimestamp' => Time.iso8601(commit['timestamp']).strftime("%FT%H:%M:%S.00Z"),
'Uri' => @chgset_uri,
'Artifacts' => artifact_refs # [{'_ref' => 'defect/1324.js'}, {}...]
}
changeset.delete('Author') if user_ref == ""

begin
changeset_item = rallyCreate('Changeset', changeset)
chgset_ref = itemRef(changeset_item)
rescue Faraday::Error => boom # or some other sort of Faraday::Error::xxxError
raise_config_error("Unable to create Rally Changeset")
# changeset_item = nil
end
return if changeset_item.nil?

# change has changeset_ref, Action, PathAndFilename, Uri
changes = []
commit['added'].each { |add| changes << {'Action' => 'A', 'PathAndFilename' => add } }
commit['modified'].each { |mod| changes << {'Action' => 'M', 'PathAndFilename' => mod } }
commit['removed'].each { |rem| changes << {'Action' => 'R', 'PathAndFilename' => rem } }
changes.each do |change|
change['Changeset'] = chgset_ref
change['Uri'] = '%s/blob/%s/%s' % [@chgset_uri, @branch, change['PathAndFilename']]
chg_item = rallyCreate('Change', change)
end
end

def validateWorkspace(workspace)
all_your_workspaces = rallyWorkspaces()
target_workspace = all_your_workspaces.select {|wksp| wksp['Name'] == workspace and wksp['State'] != 'Closed'}
if target_workspace.length != 1
problem = 'Config Error: target workspace: %s not available in list of workspaces associated with your credentials' % [workspace]
raise_config_error(problem)
end
return itemRef(target_workspace[0])
end

def getOrCreateRepo(repo, repo_owner)
repo_item = rallyQuery('SCMRepository', 'Name', 'Name = "%s"' % repo)
return itemRef(repo_item) unless repo_item.nil?
repo_info = { 'Workspace' => @wksp_ref, 'Name' => repo, 'SCMType' => 'GitHub',
'Description' => 'GitHub-Service push changesets',
'Uri' => 'http://github.com/%s/%s' % [repo_owner, repo]
}
repo_item = rallyCreate('SCMRepository', repo_info)
return itemRef(repo_item)
end

def itemRef(item) ref = item['_ref'].split('/')[-2..-1].join('/')[0..-4] end

def rallyWorkspaces()
response = @http.get('Subscription.js?fetch=Name,Workspaces,Workspace&pretty=true')
raise_config_error('config error') unless response.success?
qr = JSON.parse(response.body)
begin
workspaces = qr['Subscription']['Workspaces']
rescue Exception => ex
raise_config_error('Config error: No such workspace for your credentials')
end
return workspaces
end

def rallyQuery(entity, fields, criteria)
target_url = '%s.js?fetch=%s' % [entity.downcase, fields]
target_url += '&query=(%s)' % [criteria] if criteria.length > 0
target_url += '&workspace=%s' % [@wksp_ref]
res = @http.get(target_url)
raise StandardError("Config Error, #{entity} query failed") unless res.success?
qr = JSON.parse(res.body)['QueryResult']
item = qr['TotalResultCount'] > 0 ? qr['Results'][0] : nil
return item
end

def rallyCreate(entity, data)
create_url = "%s/create.js?workspace=%s" % [entity, @wksp_ref]
payload = {"#{entity}" => data}
res = @http.post(create_url, payload.to_json)
raise_config_error("Unable to create the Rally #{entity} for #{data['Name']}") unless res.success?
cr = JSON.parse(res.body)['CreateResult']
item = cr['Object']
return item
end

def snarfArtifacts(message)
words = message.gsub(',', ' ').gsub('\r\n', '\n').gsub('\n', ' ').gsub('\t', ' ').split(' ')
#rally_formatted_ids = words.select { |word| word =~ /^(D|DE|DS|TA|TC|S|US)\d+[\.:;]?$/ }
rally_formatted_ids = words.select { |word| @artifact_detector.match(word) }
artifacts = [] # actually, just the refs
rally_formatted_ids.uniq.each do |fmtid|
next unless fmtid =~ /^(([A-Z]{1,2})\d+)[\.:;]?$/
fmtid, prefix = $1, $2
entity = @art_type[prefix]
artifact = rallyQuery(entity, 'Name', 'FormattedID = "%s"' % fmtid)
next if artifact.nil?
art_ref = itemRef(artifact)
artifacts << {'_ref' => art_ref}
end
return artifacts
end

end

0 comments on commit f518a17

Please sign in to comment.