mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
script/ptl-tool: add --qe-label option for automated QA workflows
Introduce the `--qe-label <pr-label>` option to streamline daily integration testing and Redmine QA tracking for backports. Using `--qe-label` automatically implies `--integration` and `--pr-label`, and manages Redmine QA tracking tickets via custom field 45 (Ceph PR Label): * Searches for open QA tickets matching the specified PR label and target release branch. * If exactly one open ticket is found, prompts the user to either update the existing ticket or create a new one. * If no open ticket is found, automatically defaults to creating a new QA ticket with the label stored in its metadata. * Adds pre-flight validation to ensure all labeled PRs target the exact same base branch, preventing ambiguous or mixed-release merges. * Halts execution if multiple ambiguous open QA tickets exist for the same label and release branch. Signed-off-by: Patrick Donnelly <pdonnell@ibm.com>
This commit is contained in:
parent
aaeb264d29
commit
6f306f79e5
@ -1832,7 +1832,7 @@ def build_branch(args):
|
||||
G = git.Repo(args.git)
|
||||
|
||||
R = None
|
||||
if args.create_qa or args.update_qa or args.audit or args.final_merge:
|
||||
if args.create_qa or args.update_qa or args.audit or args.final_merge or args.qe_label:
|
||||
log.info("connecting to %s", REDMINE_ENDPOINT)
|
||||
R = Redmine(REDMINE_ENDPOINT, username=REDMINE_USER, key=REDMINE_API_KEY)
|
||||
log.debug("connected")
|
||||
@ -1857,18 +1857,32 @@ def build_branch(args):
|
||||
prs.append(n)
|
||||
log.info("Will merge PRs: {}".format(prs))
|
||||
|
||||
# PRE-FLIGHT: Auto-detect base from the first PR if necessary
|
||||
if prs and base is None:
|
||||
first_pr = prs[0]
|
||||
detected_base = get_pr_info(session, first_pr).get("base", {}).get("ref")
|
||||
|
||||
# PRE-FLIGHT: Validate base consistency and auto-detect base from PRs if necessary
|
||||
if prs and (base is None or args.qe_label or args.integration):
|
||||
bases_seen = {}
|
||||
for pr_num in prs:
|
||||
ref = get_pr_info(session, pr_num).get("base", {}).get("ref")
|
||||
if not ref:
|
||||
raise SystemExit(f"Could not determine base branch for PR #{pr_num}")
|
||||
bases_seen[pr_num] = ref
|
||||
|
||||
if detected_base:
|
||||
log.info(f"Auto-detected target base from PR #{first_pr}: {detected_base}")
|
||||
unique_bases = set(bases_seen.values())
|
||||
if len(unique_bases) > 1:
|
||||
log.error("PRs target multiple different base branches! Ambiguity is not allowed.")
|
||||
for p_num, b_ref in bases_seen.items():
|
||||
log.error(f" PR #{p_num} -> targets '{b_ref}'")
|
||||
sys.exit(1)
|
||||
|
||||
detected_base = list(unique_bases)[0]
|
||||
if base is None:
|
||||
log.info(f"Auto-detected target base from PRs: {detected_base}")
|
||||
base = detected_base
|
||||
if args.merge_branch_name is False:
|
||||
merge_branch_name = detected_base
|
||||
else:
|
||||
raise SystemExit(f"Could not auto-detect base for PR #{first_pr}. Use hard-coded --base")
|
||||
elif base != detected_base:
|
||||
log.error(f"Provided --base '{base}' does not match the target base branch '{detected_base}' of the PRs!")
|
||||
sys.exit(1)
|
||||
|
||||
if args.integration:
|
||||
if not base or base == 'HEAD':
|
||||
@ -1881,6 +1895,56 @@ def build_branch(args):
|
||||
args.always_fetch = True
|
||||
args.skip_conflict_check = True
|
||||
|
||||
if args.qe_label and not args.update_qa:
|
||||
log.info(f"Searching Redmine for open QA tickets matching Ceph PR Label '{args.qe_label}'...")
|
||||
filters = {
|
||||
f"cf_{REDMINE_CUSTOM_FIELD_ID_CEPH_PR_LABEL}": args.qe_label,
|
||||
"status_id": "open"
|
||||
}
|
||||
open_tickets = list(R.issue.filter(**filters))
|
||||
|
||||
matching_tickets = []
|
||||
for t in open_tickets:
|
||||
t_release = get_custom_field(t, REDMINE_CUSTOM_FIELD_ID_QA_RELEASE)
|
||||
if t_release and t_release != base:
|
||||
log.error(f"Open QA ticket #{t.id} ({REDMINE_ENDPOINT}/issues/{t.id}) uses label '{args.qe_label}' but targets release '{t_release}' (expected '{base}')! Release mismatch not allowed.")
|
||||
sys.exit(1)
|
||||
matching_tickets.append(t)
|
||||
|
||||
if len(matching_tickets) > 1:
|
||||
log.error(f"Ambiguity error: Found {len(matching_tickets)} open QA tickets matching label '{args.qe_label}' for release '{base}':")
|
||||
for t in matching_tickets:
|
||||
log.error(f" #{t.id}: {t.subject} ({REDMINE_ENDPOINT}/issues/{t.id})")
|
||||
sys.exit(1)
|
||||
elif len(matching_tickets) == 1:
|
||||
t = matching_tickets[0]
|
||||
t_url = f"{REDMINE_ENDPOINT}/issues/{t.id}"
|
||||
print(f"\nFound existing open QA ticket: #{t.id} - {t.subject}")
|
||||
print(f"Link: {t_url}")
|
||||
while True:
|
||||
ans = input("Do you want to update this existing QA ticket? [y/n/o/q] (y=update existing, n=create new, o=open in browser, q=quit): ").strip().lower()
|
||||
if ans == 'y':
|
||||
args.update_qa = t.id
|
||||
log.info(f"Will update existing QA ticket #{t.id}.")
|
||||
break
|
||||
elif ans == 'n':
|
||||
log.info("Will create a new QA ticket instead.")
|
||||
args.create_qa = True
|
||||
break
|
||||
elif ans == 'o':
|
||||
open_in_browser([t_url])
|
||||
print(f"Opened {t_url} in browser.")
|
||||
elif ans == 'q':
|
||||
log.info("Exiting script.")
|
||||
sys.exit(0)
|
||||
elif ans == '':
|
||||
continue
|
||||
else:
|
||||
print("Invalid choice. Please enter y, n, o, or q.")
|
||||
else:
|
||||
log.info("No open QA tickets found for this label. Will create a new QA ticket.")
|
||||
args.create_qa = True
|
||||
|
||||
if args.credits:
|
||||
try:
|
||||
c = resolve_ref(G, 'main', BASE_REMOTE_URL, args.always_fetch)
|
||||
@ -2156,6 +2220,7 @@ def main():
|
||||
group = parser.add_argument_group('GitHub PR Options')
|
||||
group.add_argument('--label', dest='label', action='store', default=default_label, help='label PRs for testing')
|
||||
group.add_argument('--pr-label', dest='pr_label', action='store', help='source PRs to merge via label')
|
||||
group.add_argument('--qe-label', dest='qe_label', action='store', help='variant of --integration that merges PRs by label and creates/updates an associated Redmine QA ticket')
|
||||
|
||||
group = parser.add_argument_group('Branch Control Options')
|
||||
group.add_argument('--always-fetch', dest='always_fetch', action='store_true', help='always fetch commits from remote (bypass local cache)')
|
||||
@ -2211,6 +2276,13 @@ def main():
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.qe_label:
|
||||
if args.ci_mode:
|
||||
log.error("--qe-label cannot be used with --ci-mode at this time.")
|
||||
sys.exit(1)
|
||||
args.integration = True
|
||||
args.pr_label = args.qe_label
|
||||
|
||||
# Make --audit-label redundant when --ci-mode is invoked
|
||||
if args.ci_mode and not args.audit_label:
|
||||
args.audit_label = parse_audit_labels("releng-audit-pass,releng-audit-fail")
|
||||
@ -2272,7 +2344,7 @@ def main():
|
||||
log.error("or set the PTL_TOOL_GITHUB_TOKEN environment variable.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.create_qa or args.update_qa or args.audit or args.final_merge:
|
||||
if args.create_qa or args.update_qa or args.audit or args.final_merge or args.qe_label:
|
||||
if Redmine is None:
|
||||
log.error("redmine library is not available so cannot create qa tracker ticket or audit")
|
||||
sys.exit(1)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user