eff4c5c82d82b41a6dc377aa4c48b65c1a4ce135 lrnassar Mon Apr 13 18:30:27 2026 -0700 Add track ticket custom field support to redmineCli update subcommand. refs #37339 New named options: --release-log-text, --release-log-url, --released-to-rr, --file-list, --table-list, --assemblies. Also adds generic --custom-field ID=VALUE for arbitrary custom fields. diff --git src/utils/redmineCli src/utils/redmineCli index 909628e2c0b..1dfa438819c 100755 --- src/utils/redmineCli +++ src/utils/redmineCli @@ -1,26 +1,28 @@ #!/usr/bin/env python3 """Comprehensive Redmine CLI for the UCSC Genome Browser team. Reads the Redmine API key from ~/.hg.conf (redmine.apiKey=...). Usage: redmineCli show 33571 redmineCli list --project maillists --status open --limit 10 redmineCli create --subject "Bug report" --description "Details here" redmineCli comment 33571 --message "Adding a note" redmineCli update 33571 --status 1 --assigned-to lou --note "Reopening" + redmineCli update 33571 --release-log-url "../cgi-bin/hgTrackUi?db=hg38&g=myTrack" + redmineCli update 33571 --custom-field 46="some value" redmineCli attach 33571 screenshot.png --note "See attached" redmineCli relate 10316 15336 30368 # relate tickets to each other redmineCli watch 37339 lou braney # add watchers to a ticket redmineCli users # list user names and IDs """ import argparse import json import mimetypes import os import re import sys import tempfile import urllib.error import urllib.parse @@ -91,30 +93,38 @@ "hibernating": 32, "preview1": 17, "preview2": 18, "final build": 19, "rejected": 6, "verified": 23, "released": 12, "closed": 5, "reopened": 31, } CF_CATEGORY = 28 CF_EMAIL = 40 CF_MLM = 9 +# Track ticket custom fields +CF_RELEASE_LOG_TEXT = 48 +CF_RELEASE_LOG_URL = 46 +CF_RELEASED_TO_RR = 47 +CF_FILE_LIST = 43 +CF_TABLE_LIST = 44 +CF_ASSEMBLIES = 2 + # Name -> Redmine user ID mapping (short names and full names) USER_IDS = { "ana": 174, "ana benet": 174, "angie": 34, "angie hinrichs": 34, "ann": 3, "ann zweig": 3, "bob": 45, "bob kuhn": 45, "blee": 122, "brian lee": 122, "braney": 31, "brian raney": 31, "build": 197, "build meister": 197, "cath": 155, "cath tyner": 155, "charlie": 186, "charlie vaske": 186, "chin": 25, "chin li": 25, "chris": 152, "chris eisenhart": 152, "christopher": 156, "christopher lee": 156, @@ -592,41 +602,62 @@ if args.assigned_to is not None: if args.assigned_to == "": issue_data["assigned_to_id"] = "" else: issue_data["assigned_to_id"] = resolve_user(args.assigned_to) if args.priority is not None: issue_data["priority_id"] = args.priority if args.subject is not None: issue_data["subject"] = strip_emoji(args.subject) custom_fields = [] if args.category is not None: custom_fields.append({"id": CF_CATEGORY, "value": args.category}) if args.mlm is not None: custom_fields.append({"id": CF_MLM, "value": args.mlm}) + if args.release_log_text is not None: + custom_fields.append({"id": CF_RELEASE_LOG_TEXT, "value": args.release_log_text}) + if args.release_log_url is not None: + custom_fields.append({"id": CF_RELEASE_LOG_URL, "value": args.release_log_url}) + if args.released_to_rr is not None: + custom_fields.append({"id": CF_RELEASED_TO_RR, "value": args.released_to_rr}) + if args.file_list is not None: + custom_fields.append({"id": CF_FILE_LIST, "value": args.file_list}) + if args.table_list is not None: + custom_fields.append({"id": CF_TABLE_LIST, "value": args.table_list}) + if args.assemblies is not None: + custom_fields.append({"id": CF_ASSEMBLIES, "value": args.assemblies}) + for cf_spec in (args.custom_field or []): + if "=" not in cf_spec: + sys.exit(f"Error: --custom-field must be ID=VALUE, got: {cf_spec}") + cf_id, cf_val = cf_spec.split("=", 1) + if not cf_id.isdigit(): + sys.exit(f"Error: custom field ID must be numeric, got: {cf_id}") + custom_fields.append({"id": int(cf_id), "value": cf_val}) if custom_fields: issue_data["custom_fields"] = custom_fields note = read_text_input(args.note, args.note_file) if note: issue_data["notes"] = strip_emoji(prepend_attribution(note)) if not issue_data: sys.exit("Error: no fields to update. Provide at least one of: " "--status, --assigned-to, --priority, --subject, " - "--category, --mlm, --note") + "--category, --mlm, --release-log-text, --release-log-url, " + "--released-to-rr, --file-list, --table-list, --assemblies, " + "--custom-field, --note") data = {"issue": issue_data} api_put(args.base_url, f"/issues/{args.ticket_id}.json", args.api_key, data) print(f"Updated #{args.ticket_id}: {make_url(args.base_url, args.ticket_id)}") # --------------------------------------------------------------------------- # Subcommand: attach # --------------------------------------------------------------------------- def cmd_attach(args): """Upload an attachment to a ticket.""" filepath = args.file if not os.path.isfile(filepath): sys.exit(f"Error: file not found: {filepath}") @@ -834,30 +865,45 @@ p_comment.add_argument("ticket_id", help="Ticket ID number") p_comment.add_argument("--message", help="Comment text") p_comment.add_argument("--message-file", dest="message_file", help="Read comment from file (- for stdin)") # update p_update = sub.add_parser("update", help="Update ticket fields") p_update.add_argument("ticket_id", help="Ticket ID number") p_update.add_argument("--status", help="New status name or ID (e.g. 'QA Ready' or 10)") p_update.add_argument("--assigned-to", dest="assigned_to", help="Assignee name/ID (empty string to clear)") p_update.add_argument("--priority", type=int, help="New priority ID") p_update.add_argument("--subject", help="New subject") p_update.add_argument("--category", help="MLQ Category") p_update.add_argument("--mlm", help="MLM name") + p_update.add_argument("--release-log-text", dest="release_log_text", + help="Release Log Text (custom field)") + p_update.add_argument("--release-log-url", dest="release_log_url", + help="Release Log URL (custom field)") + p_update.add_argument("--released-to-rr", dest="released_to_rr", + help="Released to RR (custom field, 0 or 1)") + p_update.add_argument("--file-list", dest="file_list", + help="File List (custom field)") + p_update.add_argument("--table-list", dest="table_list", + help="Table List (custom field)") + p_update.add_argument("--assemblies", + help="Assemblies (custom field)") + p_update.add_argument("--custom-field", dest="custom_field", + action="append", metavar="ID=VALUE", + help="Set arbitrary custom field by ID (repeatable)") p_update.add_argument("--note", help="Comment to include with update") p_update.add_argument("--note-file", dest="note_file", help="Read note from file (- for stdin)") # attach p_attach = sub.add_parser("attach", help="Upload an attachment") p_attach.add_argument("ticket_id", help="Ticket ID number") p_attach.add_argument("file", help="File path to upload") p_attach.add_argument("--filename", help="Override filename") p_attach.add_argument("--description", help="Attachment description") p_attach.add_argument("--note", help="Comment to add with attachment") # users p_users = sub.add_parser("users", help="List project members and their user IDs") p_users.add_argument("--project", default=DEFAULT_PROJECT,