#!/usr/bin/env python ################################################################################################### # File: attachToHelixALM.py # # Description: # Retrieves changelist information from Helix Versioning Engine, parses it, and: # - Verifies that any referenced Helix ALM items exist # OR # - Attaches the changelist to any referenced Helix ALM items # # Copyright: # Copyright © 2017 Perforce Software, Inc. # All contents of this file are considered Perforce Software proprietary. ################################################################################################### import sys import urllib.request import re import json import P4 import ssl ################################# # Begin Configuration variables # ################################# # Url of the ttextpro.exe cgi on your web server EXTERNAL_PROVIDER_URL = 'http://localhost/cgi-bin/ttextpro.exe' # SSL Context to use if using https # When using a self signed certificate you will receive a SSL: CERTIFICATE_VERIFY_FAILED error # If the certificate is trusted, change the ssl.CERT_REQUIRED to ssl.CERT_NONE SSL_CERTIFICATE_MODE = ssl.CERT_REQUIRED # SSL Protocol supported # By default PROTOCOL_SSLV23 supports SSL2, SSL3, TLS, and SSL SSL_PROTOCOL_MODE = ssl.PROTOCOL_SSLv23 # Provider key from the Source Control Providers dialog PROVIDER_KEY = '{17222e8b-a918-49cb-89b4-9f57a18cecd7}:{10b03082-31d5-4d88-99ed-7f2afef9d225}' # The P4 server port number (as string) P4PORT = '1666' # The P4 user to use when this trigger connects to the server P4USER = '' # The P4 user's password to use when this trigger connects to the server P4PASSWD = '' # Tag strings for issues, test cases and requirements. ISSUE_TAG = 'IS' TEST_CASE_TAG = 'TC' REQUIREMENT_TAG = 'RQ' ############################### # End Configuration variables # ############################### # Goal: # Get this from "p4 describe <changelist#>", where <changelist#> is given on the command line. #[ # { # 'depotFile': ['//streamsDepot/mainline/workspaceFolder/file.txt', '//streamsDepot/mainline/workspaceFolder/some other file.txt'], # 'action': ['edit', 'edit'], # 'client': 'client_4906', # 'change': '5', # 'path': '//streamsDepot/mainline/workspaceFolder/*', # 'changeType': 'public', # 'fileSize': ['23', '22'], # 'status': 'submitted', # 'time': '1485266141', # 'digest': ['EE318E40390EBB995DF2B15DE6AECC65', 'D143247FECB65C4E4AA9E70D98AC9E65'], # 'user': 'administrator', # 'rev': ['5', '2'], # 'desc': 'Fixed typo in error message [IS-15]', # 'type': ['text', 'text'] # } #] # # Build something like this and send it to the External Provider CGI: # #{ # "action" : "addAttachment" # "providerKey" : "{12345678-9abc-01d2-3456-78e901fg2345}:{67890123-4567-4662-890h-i1j23k456lm7}", # "attachmentList" : [ # array here implies changelist # { # "attachmentMessage" : "Fixed typo in error message [IS-15]", # "attachmentAuthor" : "administrator", # "toAttachList" : [ # { # "objectType" : "issue", # "number" : 15 # },... # ], # "fileList" : [ # { # "fileName" : "testDirectory/file.js", # "changeType" : "added" # },... # ] # },... # ] #} # # And send to the Helix ALM External Provider CGI. EXIT_SUCCESS = 0 EXIT_FAILURE = 1 # Tags used to attach to issues, test cases, and requirements. Format is "[TAG-NUMBER]". TAG_MAP = {ISSUE_TAG: 'issue', TEST_CASE_TAG: 'test case', REQUIREMENT_TAG: 'requirement'} TAG_PATTERN = r'\[(%s|%s|%s)-(\d+)\]' % (ISSUE_TAG, TEST_CASE_TAG, REQUIREMENT_TAG) TAG_REGEX = re.compile(TAG_PATTERN, re.IGNORECASE) # Map of uppercased Helix file 'action' strings to Helix ALM 'change type' strings. STATUS_MAP = {'ADD': 'added', 'EDIT': 'modified', 'DELETE': 'removed', 'BRANCH': 'added', 'MOVE/ADD': 'added', 'MOVE/DELETE': 'removed', 'INTEGRATE': 'modified', 'IMPORT': 'added'} # Runs a P4 command and returns the resulting dictionary def p4Run(cmd, params): p4 = P4.P4() p4.port = P4PORT p4.user = P4USER p4.password = P4PASSWD result = None try: p4.connect() if p4.connected() == True: if p4.server_unicode == True: p4.charset = 'utf8' result = p4.run(cmd, params) p4.disconnect() except P4.P4Exception: print("Exception Occurred") if len(p4.errors) > 0: for err in p4.errors: print(err) if result == None: print("Unable to execute Helix command " + cmd) exit(EXIT_FAILURE) return result # Returns the p4 description of the changelist specific by the passed number def p4DescribeChangeList(changelistNum): return p4Run('describe', ['-s',changelistNum]) # Maps the core item tag string ("IS"/"TC"/"RQ") to the longer name expected by the Helix ALM CGI side ("issue", etc..) def getObjectType(tag): longName = None if tag.upper() in TAG_MAP: longName = TAG_MAP[tag.upper()] return longName # Maps the p4 file 'action' to the 'changeType' strings Helix ALM expects. def getChangeType(status): changeType = None if status.upper() in STATUS_MAP: changeType = STATUS_MAP[status.upper()] return changeType # Parses the given text for "[IS-18]" and similar Helix ALM tags, # returns [ { 'objectType' : '...', 'number' : # }, ...] that represents those Helix ALM objects we want to attach to. def parseALMTagsFromString(str): matches = TAG_REGEX.finditer(str) almItems = [] for match in matches: almItems.append({'objectType': getObjectType(match.group(1)), 'number': int(match.group(2))}) return almItems # Convert P4's 'depotFile', 'action', and 'rev' parallel arrays into # [ { 'fileName' : '<depotFileN>', 'changeType': '<actionN>', 'version' : <revN> }, ...] that Helix ALM expects. # Side-note: Since this is sent as a JSON array, it is interpretted as a changelist on the Helix ALM side. def buildALMSCCFileList(p4Description): depFileLen = len(p4Description['depotFile']) actionLen = len(p4Description['action']) revLen = len(p4Description['rev']) if depFileLen != actionLen or depFileLen != revLen: print("Failed to parse changelist description") exit(EXIT_FAILURE) fileList = [] for x in range(0, len(p4Description['depotFile'])): fileName = p4Description['depotFile'][x] if fileName != None: fileList.append({ 'fileName' : fileName, 'changeType' : getChangeType(p4Description['action'][x]), 'version' : p4Description['rev'][x] } ) return fileList # Builds a basic Helix ALM external provider CGI request with the given request 'action' value, # not to be confused with the P4 'action' value def buildALMRequest(action): return { 'providerKey' : PROVIDER_KEY, 'action' : action } # Builds a Helix ALM add attachment request to attach the p4 described changelist. def buildALMAttachRequest(p4Description, bVerifyOnly): almItems = parseALMTagsFromString(p4Description['desc']) fileList = [] if bVerifyOnly == False and 'depotFile' in p4Description: fileList = buildALMSCCFileList(p4Description) if len(almItems) <= 0 or (bVerifyOnly == False and len(fileList) <= 0): exit(EXIT_SUCCESS) # No files to attach or no items to attach to. Nothing to do. requestType = 'AddAttachment' if bVerifyOnly == True: requestType = 'VerifyAttachment' # this dictionary will contain the AddAttachment request. It will be converted # to JSON and sent to the URL configured above. attachRequest = buildALMRequest(requestType) attachRequest['attachmentList'] = [ { 'attachmentMessage' : str(p4Description['desc']), 'attachmentAuthor' : p4Description['user'], 'toAttachList' : almItems, 'fileList' : fileList, 'changeListNum' : str(p4Description['change']), } ] return attachRequest # Sends the given request to the external provider CGI, never returns # exits with either 0 on success or the HTTP status code. def sendALMExternalProviderRequest(almRequest): almRequestJsonStr = json.dumps(almRequest) # encode the python objects as an HTTP request with json. httpRequest = urllib.request.Request(EXTERNAL_PROVIDER_URL, almRequestJsonStr.encode(), {'Content-Type': 'application/json'}) # setup the ssl context sslcontext = ssl.SSLContext(SSL_PROTOCOL_MODE) sslcontext.verify_mode = SSL_CERTIFICATE_MODE # send the HTTP request to the web server. return urllib.request.urlopen(httpRequest, context=sslcontext) # Requests description from P4, builds an 'AddAttachment' request and submits # it to the Helix ALM external provider CGI. Returns the HTTP status or # exits if an error occurs before then. def attachP4ChangelistToHelixALM(changeListNum, bVerifyOnly): p4Descriptions = p4DescribeChangeList(changeListNum) if isinstance(p4Descriptions, list) == False or len(p4Descriptions) <= 0: print("Changelist description not found.") exit(EXIT_FAILURE) attachRequest = buildALMAttachRequest(p4Descriptions[0], bVerifyOnly) return sendALMExternalProviderRequest(attachRequest) # Attaches a P4 changelist to one or more Helix ALM items. # Never returns, exits with 0 on success or 1 on failure def p4TriggerAttachToHelixALM(changeListNum, bVerifyOnly): httpResp = attachP4ChangelistToHelixALM(changeListNum, bVerifyOnly) if httpResp.status == 200: jsonResp = json.loads(httpResp.read().decode()) if jsonResp['errorCode'] == 0: exit(EXIT_SUCCESS) elif 'errorList' in jsonResp: for err in jsonResp['errorList']: print(err['errorMessage']) elif 'errorMessage' in jsonResp: print(jsonResp['errorMessage']) else: print("HTTP request failed: " + str(httpStatus)) exit(EXIT_FAILURE) # Main if __name__ == '__main__': # 0 is always the script name. 1 = changelist number, 2 = optional "verify" if len(sys.argv) < 2: print("Missing changelist number parameter") exit(EXIT_FAILURE) bVerifyOnly = len(sys.argv) > 2 and sys.argv[2] == 'verify' p4TriggerAttachToHelixALM(sys.argv[1], bVerifyOnly)