33# README:
44#
55# This tool's purpose is to make it easier to merge PRs into test branches and
6- # into main. Make sure you generate a Personal access token in GitHub and
7- # add it your ~/.github.key.
6+ # into main.
7+ #
8+ #
9+ # You will probably want to setup a virtualenv for running this script:
10+ #
11+ # (
12+ # virtualenv3 ~/ptl-venv
13+ # source ~/ptl-venv/bin/activate
14+ # pip3 install GitPython
15+ # pip3 install python-redmine
16+ # )
17+ #
18+ # Then run the tool with:
19+ #
20+ # (source ~/ptl-venv/bin/activate && python3 src/script/ptl-tool.py ...)
21+ #
22+ #
23+ # Some important environment variables:
24+ #
25+ # - PTL_TOOL_BASE_REMOTE (the name for your upstream remote, default "upstream")
26+ # - PTL_TOOL_GITHUB_USER (your github username)
27+ # - PTL_TOOL_GITHUB_API_KEY (your github api key, or what is stored in ~/.github.key)
28+ # - PTL_TOOL_REDMINE_USER (your redmine username)
29+ # - PTL_TOOL_REDMINE_API_KEY (your redmine api key, or what is stored in ~/redmine_key)
30+ # - PTL_TOOL_USER (your desired username embedded in test branch names)
31+ #
32+ # Make a redmine API key on the right side of https://tracker.ceph.com/my/account
833#
934# Because developers often have custom names for the ceph upstream remote
1035# (https://github.com/ceph/ceph.git), You will probably want to export the
1742#
1843# export PTL_TOOL_BASE_REMOTE=<remotename>
1944#
45+ #
46+ # You can use this tool to create a QA tracker ticket for you:
47+ #
48+ # $ python3 ptl-tool.py ... --create-qa --qa-release reef
49+ #
50+ # which will populate the ticket with all the usual information and also push a
51+ # tagged version of your test branch to ceph-ci for posterity.
52+
2053#
2154# ** Here are some basic exmples to get started: **
2255#
100133
101134# TODO
102135# Look for check failures?
103- # redmine issue update: http://www.redmine.org/projects/redmine/wiki/Rest_Issues
104136
105137import argparse
106138import codecs
107139import datetime
108- import getpass
109- import git
140+ from getpass import getuser
141+ import git # https://github.com/gitpython-developers/gitpython
110142import itertools
111143import json
112144import logging
113145import os
114146import re
147+ try :
148+ from redminelib import Redmine # https://pypi.org/project/python-redmine/
149+ except ModuleNotFoundError :
150+ Redmine = None
115151import requests
152+ import signal
116153import sys
117154
118155from os .path import expanduser
119156
120- log = logging .getLogger (__name__ )
121- log .addHandler (logging .StreamHandler ())
122- log .setLevel (logging .INFO )
123-
124157BASE_PROJECT = os .getenv ("PTL_TOOL_BASE_PROJECT" , "ceph" )
125158BASE_REPO = os .getenv ("PTL_TOOL_BASE_REPO" , "ceph" )
126159BASE_REMOTE = os .getenv ("PTL_TOOL_BASE_REMOTE" , "upstream" )
127160BASE_PATH = os .getenv ("PTL_TOOL_BASE_PATH" , "refs/remotes/upstream/" )
128161GITDIR = os .getenv ("PTL_TOOL_GITDIR" , "." )
129- USER = os .getenv ("PTL_TOOL_USER" , getpass .getuser ())
130- with open (expanduser ("~/.github.key" )) as f :
131- PASSWORD = f .read ().strip ()
132- TEST_BRANCH = os .getenv ("PTL_TOOL_TEST_BRANCH" , "wip-{user}-testing-%Y%m%d.%H%M%S" )
133-
134- SPECIAL_BRANCHES = ('main' , 'luminous' , 'jewel' , 'HEAD' )
135-
162+ GITHUB_USER = os .getenv ("PTL_TOOL_GITHUB_USER" , os .getenv ("PTL_TOOL_USER" , getuser ()))
163+ GITHUB_API_KEY = None
164+ try :
165+ with open (expanduser ("~/.github.key" )) as f :
166+ GITHUB_API_KEY = f .read ().strip ()
167+ except FileNotFoundError :
168+ pass
169+ GITHUB_API_KEY = os .getenv ("PTL_TOOL_GITHUB_API_KEY" , GITHUB_API_KEY )
136170INDICATIONS = [
137171 re .
compile (
"(Reviewed-by: .+ <[\[email protected] ]+>)" ,
re .
IGNORECASE ),
138172 re .
compile (
"(Acked-by: .+ <[\[email protected] ]+>)" ,
re .
IGNORECASE ),
139173 re .
compile (
"(Tested-by: .+ <[\[email protected] ]+>)" ,
re .
IGNORECASE ),
140174]
175+ REDMINE_CUSTOM_FIELD_ID_SHAMAN_BUILD = 26
176+ REDMINE_CUSTOM_FIELD_ID_QA_RUNS = 27
177+ REDMINE_CUSTOM_FIELD_ID_QA_RELEASE = 28
178+ REDMINE_CUSTOM_FIELD_ID_GIT_BRANCH = 29
179+ REDMINE_ENDPOINT = "https://tracker.ceph.com"
180+ REDMINE_PROJECT_QA = "ceph-qa"
181+ REDMINE_TRACKER_QA = "QA Run"
182+ REDMINE_USER = os .getenv ("PTL_TOOL_REDMINE_USER" , getuser ())
183+ REDMINE_API_KEY = None
184+ try :
185+ with open (expanduser ("~/.redmine_key" )) as f :
186+ REDMINE_API_KEY = f .read ().strip ()
187+ except FileNotFoundError :
188+ pass
189+ REDMINE_API_KEY = os .getenv ("PTL_TOOL_REDMINE_API_KEY" , REDMINE_API_KEY )
190+ SPECIAL_BRANCHES = ('main' , 'luminous' , 'jewel' , 'HEAD' )
191+ TEST_BRANCH = os .getenv ("PTL_TOOL_TEST_BRANCH" , "wip-{user}-testing-%Y%m%d.%H%M%S" )
192+ USER = os .getenv ("PTL_TOOL_USER" , getuser ())
193+
194+ log = logging .getLogger (__name__ )
195+ log .addHandler (logging .StreamHandler ())
196+ log .setLevel (logging .INFO )
141197
142198# find containing git dir
143199git_dir = GITDIR
162218BZ_MATCH = re .compile ("(.*https?://bugzilla.redhat.com/.*)" )
163219TRACKER_MATCH = re .compile ("(.*https?://tracker.ceph.com/.*)" )
164220
221+ def gitauth ():
222+ return (GITHUB_USER , GITHUB_API_KEY )
223+
165224def get (session , url , params = None , paging = True ):
166225 if params is None :
167226 params = {}
168227 params ['per_page' ] = 100
169228
170229 log .debug (f"Fetching { url } " )
171- response = session .get (url , auth = ( USER , PASSWORD ), params = params )
230+ response = session .get (url , auth = gitauth ( ), params = params )
172231 log .debug (f"Response = { response } ; links = { response .headers .get ('link' , '' )} " )
173232 if response .status_code != 200 :
174233 log .error (f"Failed to fetch { url } : { response } " )
@@ -182,7 +241,7 @@ def get(session, url, params=None, paging=True):
182241 log .debug (f"Fetching { url } " )
183242 new_params = dict (params )
184243 new_params .update ({'page' : page })
185- response = session .get (url , auth = ( USER , PASSWORD ), params = new_params )
244+ response = session .get (url , auth = gitauth ( ), params = new_params )
186245 log .debug (f"Response = { response } ; links = { response .headers .get ('link' , '' )} " )
187246 if response .status_code != 200 :
188247 log .error (f"Failed to fetch { url } : { response } " )
@@ -271,6 +330,11 @@ def build_branch(args):
271330
272331 G = git .Repo (args .git )
273332
333+ if args .create_qa :
334+ log .info ("connecting to %s" , REDMINE_ENDPOINT )
335+ R = Redmine (REDMINE_ENDPOINT , username = REDMINE_USER , key = REDMINE_API_KEY )
336+ log .debug ("connected" )
337+
274338 # First get the latest base branch and PRs from BASE_REMOTE
275339 remote = getattr (G .remotes , BASE_REMOTE )
276340 remote .fetch ()
@@ -310,6 +374,8 @@ def build_branch(args):
310374 G .git .checkout (c )
311375 assert G .head .is_detached
312376
377+ qa_tracker_description = []
378+
313379 for pr in prs :
314380 pr = int (pr )
315381 log .info ("Merging PR #{pr}" .format (pr = pr ))
@@ -324,6 +390,8 @@ def build_branch(args):
324390 endpoint = f"https://api.github.com/repos/{ BASE_PROJECT } /{ BASE_REPO } /pulls/{ pr } "
325391 response = next (get (session , endpoint , paging = False ))
326392
393+ qa_tracker_description .append (f'* "PR #{ pr } ":{ response ["html_url" ]} -- { response ["title" ].strip ()} ' )
394+
327395 message = "Merge PR #%d into %s\n \n * %s:\n " % (pr , merge_branch_name , remote_ref )
328396
329397 for commit in G .iter_commits (rev = "HEAD.." + str (tip )):
@@ -354,12 +422,23 @@ def build_branch(args):
354422 G .git .commit ("--amend" , "--no-edit" )
355423
356424 if label :
357- req = session .post ("https://api.github.com/repos/{project}/{repo}/issues/{pr}/labels" .format (pr = pr , project = BASE_PROJECT , repo = BASE_REPO ), data = json .dumps ([label ]), auth = ( USER , PASSWORD ))
425+ req = session .post ("https://api.github.com/repos/{project}/{repo}/issues/{pr}/labels" .format (pr = pr , project = BASE_PROJECT , repo = BASE_REPO ), data = json .dumps ([label ]), auth = gitauth ( ))
358426 if req .status_code != 200 :
359427 log .error ("PR #%d could not be labeled %s: %s" % (pr , label , req ))
360428 sys .exit (1 )
361429 log .info ("Labeled PR #{pr} {label}" .format (pr = pr , label = label ))
362430
431+ if args .stop_at_built :
432+ log .warning ("Stopping execution (SIGSTOP) with built branch for further modification. Foreground when execution should resume (typically `fg`)." )
433+ old_head = G .head .commit
434+ signal .raise_signal (signal .SIGSTOP )
435+ log .warning ("Resuming execution." )
436+ new_head = G .head .commit
437+ if old_head != new_head :
438+ rev = f'{ old_head } ..{ new_head } '
439+ for commit in G .iter_commits (rev = rev ):
440+ qa_tracker_description .append (f'* "commit { commit } ":https://github.com/ceph/ceph-ci/commit/{ commit } -- { commit .summary } ' )
441+
363442 # If the branch is 'HEAD', leave HEAD detached (but use "main" for commit message)
364443 if branch == 'HEAD' :
365444 log .info ("Leaving HEAD detached; no branch anchors your commits" )
@@ -375,10 +454,48 @@ def build_branch(args):
375454
376455 if created_branch :
377456 # tag it for future reference.
378- tag = "testing/%s" % branch
379- git .refs .tag .Tag .create (G , tag )
457+ tag_name = "testing/%s" % branch
458+ tag = git .refs .tag .Tag .create (G , tag_name )
380459 log .info ("Created tag %s" % tag )
381460
461+ if args .create_qa :
462+ if created_branch is None :
463+ log .error ("branch already exists!" )
464+ sys .exit (1 )
465+ project = R .project .get (REDMINE_PROJECT_QA )
466+ log .debug ("got redmine project %s" , project )
467+ user = R .user .get ('current' )
468+ log .debug ("got redmine user %s" , user )
469+ for tracker in project .trackers :
470+ if tracker ['name' ] == REDMINE_TRACKER_QA :
471+ tracker = tracker
472+ if tracker is None :
473+ log .error ("could not find tracker in project: %s" , REDMINE_TRACKER_QA )
474+ log .debug ("got redmine tracker %s" , tracker )
475+
476+ # Use hard-coded custom field ids because there is apparently no way to
477+ # figure these out via the python library
478+ custom_fields = []
479+ custom_fields .append ({'id' : REDMINE_CUSTOM_FIELD_ID_SHAMAN_BUILD , 'value' : branch })
480+ custom_fields .append ({'id' : REDMINE_CUSTOM_FIELD_ID_QA_RUNS , 'value' : branch })
481+ custom_fields .append ({'id' : REDMINE_CUSTOM_FIELD_ID_QA_RELEASE , 'value' : args .qa_release })
482+
483+ G .remotes .ci .push (tag )
484+ origin_url = f'ceph/ceph-ci/commits/{ tag .name } '
485+ custom_fields .append ({'id' : REDMINE_CUSTOM_FIELD_ID_GIT_BRANCH , 'value' : origin_url })
486+
487+ issue_kwargs = {
488+ "assigned_to_id" : user ['id' ],
489+ "custom_fields" : custom_fields ,
490+ "description" : '\n ' .join (qa_tracker_description ),
491+ "project_id" : project ['id' ],
492+ "subject" : branch ,
493+ "watcher_user_ids" : user ['id' ],
494+ }
495+ log .debug ("creating issue with kwargs: %s" , issue_kwargs )
496+ issue = R .issue .create (** issue_kwargs )
497+ log .info ("created redmine qa issue: %s" , issue .url )
498+
382499def main ():
383500 parser = argparse .ArgumentParser (description = "Ceph PTL tool" )
384501 default_base = 'main'
@@ -392,16 +509,28 @@ def main():
392509 else :
393510 argv = sys .argv [1 :]
394511 parser .add_argument ('--branch' , dest = 'branch' , action = 'store' , default = default_branch , help = 'branch to create ("HEAD" leaves HEAD detached; i.e. no branch is made)' )
512+ parser .add_argument ('--create-qa' , dest = 'create_qa' , action = 'store_true' , help = 'create QA run ticket' )
513+ parser .add_argument ('--debug' , dest = 'debug' , action = 'store_true' , help = 'turn debugging on' )
395514 parser .add_argument ('--debug-build' , dest = 'debug_build' , action = 'store_true' , help = 'append -debug to branch name prompting ceph-build to build with CMAKE_BUILD_TYPE=Debug' )
396515 parser .add_argument ('--merge-branch-name' , dest = 'merge_branch_name' , action = 'store' , default = False , help = 'name of the branch for merge messages' )
397516 parser .add_argument ('--base' , dest = 'base' , action = 'store' , default = default_base , help = 'base for branch' )
398517 parser .add_argument ('--base-path' , dest = 'base_path' , action = 'store' , default = BASE_PATH , help = 'base for branch' )
399518 parser .add_argument ('--git-dir' , dest = 'git' , action = 'store' , default = git_dir , help = 'git directory' )
400519 parser .add_argument ('--label' , dest = 'label' , action = 'store' , default = default_label , help = 'label PRs for testing' )
401520 parser .add_argument ('--pr-label' , dest = 'pr_label' , action = 'store' , help = 'label PRs for testing' )
521+ parser .add_argument ('--qa-release' , dest = 'qa_release' , action = 'store' , default = 'main' , help = 'QA release for tracker' )
402522 parser .add_argument ('--no-credits' , dest = 'credits' , action = 'store_false' , help = 'skip indication search (Reviewed-by, etc.)' )
523+ parser .add_argument ('--stop-at-built' , dest = 'stop_at_built' , action = 'store_true' , help = 'stop execution when branch is built' )
403524 parser .add_argument ('prs' , metavar = "PR" , type = int , nargs = '*' , help = 'Pull Requests to merge' )
404525 args = parser .parse_args (argv )
526+
527+ if args .debug :
528+ log .setLevel (logging .DEBUG )
529+
530+ if args .create_qa and Redmine is None :
531+ log .error ("redmine library is not available so cannot create qa tracker ticket" )
532+ sys .exit (1 )
533+
405534 return build_branch (args )
406535
407536if __name__ == "__main__" :
0 commit comments