Skip to content

Commit 62ed3a0

Browse files
committed
Merge PR ceph#56835 into main
* refs/pull/56835/head: script/ptl-tool: create qa trackers for test branches script/ptl-tool: add switch for debugging script/ptl-tool: add --stop-at-built flag Reviewed-by: Yuri Weinstein <[email protected]>
2 parents f7624de + 4bc66fa commit 62ed3a0

File tree

1 file changed

+150
-21
lines changed

1 file changed

+150
-21
lines changed

src/script/ptl-tool.py

Lines changed: 150 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,33 @@
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
@@ -17,6 +42,14 @@
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
#
@@ -100,44 +133,67 @@
100133

101134
# TODO
102135
# Look for check failures?
103-
# redmine issue update: http://www.redmine.org/projects/redmine/wiki/Rest_Issues
104136

105137
import argparse
106138
import codecs
107139
import datetime
108-
import getpass
109-
import git
140+
from getpass import getuser
141+
import git # https://github.com/gitpython-developers/gitpython
110142
import itertools
111143
import json
112144
import logging
113145
import os
114146
import re
147+
try:
148+
from redminelib import Redmine # https://pypi.org/project/python-redmine/
149+
except ModuleNotFoundError:
150+
Redmine = None
115151
import requests
152+
import signal
116153
import sys
117154

118155
from os.path import expanduser
119156

120-
log = logging.getLogger(__name__)
121-
log.addHandler(logging.StreamHandler())
122-
log.setLevel(logging.INFO)
123-
124157
BASE_PROJECT = os.getenv("PTL_TOOL_BASE_PROJECT", "ceph")
125158
BASE_REPO = os.getenv("PTL_TOOL_BASE_REPO", "ceph")
126159
BASE_REMOTE = os.getenv("PTL_TOOL_BASE_REMOTE", "upstream")
127160
BASE_PATH = os.getenv("PTL_TOOL_BASE_PATH", "refs/remotes/upstream/")
128161
GITDIR = 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)
136170
INDICATIONS = [
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
143199
git_dir = GITDIR
@@ -162,13 +218,16 @@
162218
BZ_MATCH = re.compile("(.*https?://bugzilla.redhat.com/.*)")
163219
TRACKER_MATCH = re.compile("(.*https?://tracker.ceph.com/.*)")
164220

221+
def gitauth():
222+
return (GITHUB_USER, GITHUB_API_KEY)
223+
165224
def 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+
382499
def 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

407536
if __name__ == "__main__":

0 commit comments

Comments
 (0)