7fbdfb4c6e5566f9590c6e8ab9ddbd723d329a45 braney Mon May 18 13:17:51 2026 -0700 redmineCli: add --private flag to comment/update/attach for Redmine private notes. refs #37281 diff --git src/utils/redmineCli src/utils/redmineCli index e895f87f040..d6e8d004b8d 100755 --- src/utils/redmineCli +++ src/utils/redmineCli @@ -629,34 +629,38 @@ ticket_id = result["issue"]["id"] print(f"Created #{ticket_id}: {make_url(args.base_url, ticket_id)}") # --------------------------------------------------------------------------- # Subcommand: comment # --------------------------------------------------------------------------- def cmd_comment(args): """Add a comment to an existing ticket.""" message = read_text_input(args.message, args.message_file) if not message: sys.exit("Error: --message or --message-file is required") message = strip_emoji(prepend_attribution(message)) - data = {"issue": {"notes": message}} + issue = {"notes": message} + if args.private: + issue["private_notes"] = True + data = {"issue": issue} api_put(args.base_url, f"/issues/{args.ticket_id}.json", args.api_key, data) - print(f"Commented on #{args.ticket_id}: {make_url(args.base_url, args.ticket_id)}") + label = "private comment" if args.private else "Commented" + print(f"{label} on #{args.ticket_id}: {make_url(args.base_url, args.ticket_id)}") # --------------------------------------------------------------------------- # Subcommand: update # --------------------------------------------------------------------------- def cmd_update(args): """Update fields on an existing ticket.""" issue_data = {} if args.status is not None: issue_data["status_id"] = resolve_status(args.status) if args.assigned_to is not None: if args.assigned_to == "": issue_data["assigned_to_id"] = "" @@ -714,30 +718,34 @@ 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["custom_fields"] = custom_fields note = read_text_input(args.note, args.note_file) if note: issue_data["notes"] = strip_emoji(prepend_attribution(note)) + if args.private: + issue_data["private_notes"] = True + elif args.private: + sys.exit("Error: --private requires --note or --note-file") if not issue_data: sys.exit("Error: no fields to update. Provide at least one of: " "--status, --assigned-to, --priority, --subject, " "--target-version, --category, --mlm, " "--release-log-text, --release-log-url, " "--released-to-rr, --file-list, --file-list-add, " "--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)}") # --------------------------------------------------------------------------- @@ -755,30 +763,34 @@ file_data = f.read() token = api_upload(args.base_url, args.api_key, filename, file_data) content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" issue_data = { "uploads": [{"token": token, "filename": filename, "content_type": content_type}] } if args.description: issue_data["uploads"][0]["description"] = args.description if args.note: issue_data["notes"] = strip_emoji(prepend_attribution(args.note)) + if args.private: + issue_data["private_notes"] = True + elif args.private: + sys.exit("Error: --private requires --note") data = {"issue": issue_data} api_put(args.base_url, f"/issues/{args.ticket_id}.json", args.api_key, data) print(f"Attached {filename} to #{args.ticket_id}: " f"{make_url(args.base_url, args.ticket_id)}") # --------------------------------------------------------------------------- # Subcommand: users # --------------------------------------------------------------------------- def cmd_users(args): """List project members and their Redmine user IDs.""" users = [] offset = 0 @@ -986,30 +998,33 @@ 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)") # comment p_comment = sub.add_parser("comment", help="Add a comment to a ticket") 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)") + p_comment.add_argument("--private", action="store_true", + help="Mark this comment as a private note " + "(only visible to project members with permission)") # 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("--target-version", dest="target_version", help="Target version name or ID (empty string to clear)") 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)") @@ -1021,38 +1036,44 @@ help="File List (custom field, overwrites existing value)") p_update.add_argument("--file-list-add", dest="file_list_add", action="append", metavar="PATH", help="Append PATH to the File List custom field " "(repeatable; idempotent, skips paths already present)") 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)") + p_update.add_argument("--private", action="store_true", + help="Mark the accompanying note as a private note " + "(requires --note or --note-file)") # 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") + p_attach.add_argument("--private", action="store_true", + help="Mark the accompanying note as a private note " + "(requires --note)") # users p_users = sub.add_parser("users", help="List project members and their user IDs") p_users.add_argument("--project", default=DEFAULT_PROJECT, help="Project identifier (default: %(default)s)") # relate p_relate = sub.add_parser("relate", help="Create relations between tickets") p_relate.add_argument("ticket_ids", nargs="+", help="Two or more ticket IDs to relate") p_relate.add_argument("--type", default="relates", help="Relation type: relates, duplicates, duplicated, blocks, " "blocked, precedes, follows, copied_to, copied_from " "(default: %(default)s)") # watch