8600f5319ad0670e63cdee1f17fe2872224bd89d
braney
  Sat Jul 18 14:00:49 2026 -0700
redmineCli: support --assemblies and --custom-field on create

Trackers with required custom fields (e.g. Track requires Assemblies)
422'd on a bare create. Add a named --assemblies flag (custom field 2)
and a generic repeatable --custom-field ID=VALUE, reusing the same
parsing/validation as update. refs #37281

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

diff --git src/utils/redmineCli src/utils/redmineCli
index 65b4f781575..8130aa030f7 100755
--- src/utils/redmineCli
+++ src/utils/redmineCli
@@ -1,24 +1,26 @@
 #!/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 create --project genomebrowser --tracker 11 --subject "New track" \
+        --description "Details here" --assemblies hg38   # Track tracker requires Assemblies
     redmineCli build-patch --subject "v500 build patch: ..." --description "..." \
         --developer braney --commit-id abc123 --files src/hg/js/hgTracks.js \
         --cgis hgTracks --test-case "..." --reviewer hiram \
         --target-version 500 --relates 27113
     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 note 20460 85                   # show note-85 from ticket 20460
     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
 """
 
@@ -623,30 +625,43 @@
             "subject": subject,
             "description": description,
             "tracker_id": resolve_tracker(args.tracker),
             "priority_id": args.priority,
             "status_id": resolve_status(args.status),
         }
     }
 
     custom_fields = []
     if args.category:
         custom_fields.append({"id": CF_CATEGORY, "value": args.category})
     if args.email:
         custom_fields.append({"id": CF_EMAIL, "value": args.email})
     if args.mlm:
         custom_fields.append({"id": CF_MLM, "value": args.mlm})
+    if getattr(args, "assemblies", None) is not None:
+        custom_fields.append({"id": CF_ASSEMBLIES, "value": args.assemblies})
+    for cf_spec in (getattr(args, "custom_field", None) 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}")
+        cf_id = int(cf_id)
+        if cf_id in FIELD_VALIDATORS:
+            validator, name = FIELD_VALIDATORS[cf_id]
+            cf_val = validator(cf_val, f"--custom-field {cf_id} ({name})")
+        custom_fields.append({"id": cf_id, "value": cf_val})
     if custom_fields:
         issue_data["issue"]["custom_fields"] = custom_fields
 
     if args.assigned_to:
         issue_data["issue"]["assigned_to_id"] = resolve_user(args.assigned_to)
 
     result = api_post(args.base_url, "/issues.json", args.api_key, issue_data)
     ticket_id = result["issue"]["id"]
     print(f"Created #{ticket_id}: {make_url(args.base_url, ticket_id)}")
 
 
 # ---------------------------------------------------------------------------
 # Subcommand: build-patch
 # ---------------------------------------------------------------------------
 
@@ -1089,30 +1104,37 @@
     p_create.add_argument("--description-file", dest="description_file",
                           help="Read description from file (- for stdin)")
     p_create.add_argument("--project", default=DEFAULT_PROJECT,
                           help="Project (default: %(default)s)")
     p_create.add_argument("--tracker", default=TRACKER_MLQ,
                           help="Tracker name or ID (e.g. 'To Do' or 10; default: %(default)s)")
     p_create.add_argument("--priority", type=int, default=PRIORITY_UNPRIORITIZED,
                           help="Priority ID (default: %(default)s)")
     p_create.add_argument("--status", default=STATUS_NEW,
                           help="Status name or ID (e.g. 'New' or 1; default: %(default)s)")
     p_create.add_argument("--assigned-to", dest="assigned_to",
                           help="Assignee name or user ID")
     p_create.add_argument("--category", help="MLQ Category (custom field)")
     p_create.add_argument("--email", help="Sender email (custom field)")
     p_create.add_argument("--mlm", help="MLM name (custom field)")
+    p_create.add_argument("--assemblies",
+                          help="Assemblies (custom field, e.g. 'hg38')")
+    p_create.add_argument("--custom-field", dest="custom_field",
+                          action="append", metavar="ID=VALUE",
+                          help="Set an arbitrary custom field by numeric ID "
+                               "(repeatable). Use for required fields on trackers "
+                               "like Track that would otherwise 422.")
 
     # build-patch
     p_bp = sub.add_parser("build-patch",
                           help="Create a Build Patch ticket (GB, Urgent, all required fields set)")
     p_bp.add_argument("--subject", required=True, help="Ticket subject")
     p_bp.add_argument("--description", help="What is broken and why it needs a patch")
     p_bp.add_argument("--description-file", dest="description_file",
                       help="Read description from file (- for stdin)")
     p_bp.add_argument("--developer", required=True,
                       help="Developer who caused the bug (name or user ID)")
     p_bp.add_argument("--commit-id", dest="commit_id", required=True,
                       help="Fix commit hash; list all hashes if several commits fix it")
     p_bp.add_argument("--files", required=True,
                       help="Full paths of the source files changed")
     p_bp.add_argument("--cgis", required=True,