[PR #11735/3e9689b1 backport][stable-12] jira - resolve Cloud assignee email to account ID via user search (#11891)

jira - resolve Cloud assignee email to account ID via user search (#11735)

* jira - resolve Cloud assignee email to account ID via user search

When cloud=true and assignee contains '@', look up a unique user with
GET /rest/api/2/user/search and use accountId for create, transition,
and edit. Document Jira Cloud vs Server/Data Center assignee behavior.

Fixes https://github.com/ansible-collections/community.general/issues/11734

Assisted-by AI: Claude 4.6 Opus (Anthropic) via Cursor IDE



* * Using urllib.parse.quote for URL encoding
* Adding "added in version" note for assignee when resolving account_id from email



* * Added cached variable 'user_email'
* Changed comparison to handle missing email safely
* Updated error message formatting to use repr-style values



* jira - adjust assignee and cloud descriptions (#11734)



* jira - resolve user-type field emails to account IDs on Jira Cloud (#11734)

When cloud=true, user-type fields (assignee, reporter, and any listed
in the new custom_user_fields parameter) that contain '@' are resolved
from email to Jira Cloud account ID via the user search API. Strings
without '@' are assumed to be account IDs. Add custom_user_fields
parameter for user to declare additional custom fields of user type.



* jira - address PR 11735 review (docs, assignee path, errors, naming)

- Clarify O(custom_user_fields): built-ins stay automatic; list extra
  user-typed fields without implying they are only custom-field IDs.
- On Jira Cloud, set assignee from the module param as a plain string and
  let resolve_user_fields() map it to accountId (including email lookup).
- Drop redundant ``or []`` when merging O(custom_user_fields) with the
  built-in user field list.
- Use public names USER_FIELDS, resolve_user_fields, and resolve_account_id
  (no leading underscore) per reviewer preference.
- Quote field name and email in resolution errors with explicit "…" text
  instead of repr-style !r, keeping values readable in failure messages.

Refs: https://github.com/ansible-collections/community.general/pull/11735

AI-assisted: Composer 2 (Anthropic) via Cursor IDE



* Changing fail_json formatting



* formatting fixes



* jira - fixing assignee as module option in description



---------


(cherry picked from commit 3e9689b13d)

Signed-off-by: Vladimir Vasilev <vvasilev@redhat.com>
Co-authored-by: vladi-k <53343355+vladi-k@users.noreply.github.com>
This commit is contained in:
patchback[bot]
2026-04-20 09:36:05 +02:00
committed by GitHub
parent 03da9164d1
commit be4cf3ba4d
2 changed files with 112 additions and 10 deletions

View File

@@ -0,0 +1,7 @@
minor_changes:
- jira - when ``cloud=true``, user-type fields
(``assignee``, ``reporter``, and any listed in the new
``custom_user_fields`` parameter) containing an email
address are automatically resolved to Jira Cloud
account IDs
(https://github.com/ansible-collections/community.general/issues/11734, https://github.com/ansible-collections/community.general/pull/11735).

View File

@@ -134,7 +134,11 @@ options:
type: str
description:
- Sets the assignee when O(operation) is V(create), V(transition), or V(edit).
- Recent versions of JIRA no longer accept a user name as a user identifier. In that case, use O(account_id) instead.
- Jira Cloud requires account IDs for all user-type fields. When O(cloud=true), a string value containing C(@) is
resolved as an email to an account ID automatically. A string without C(@) is assumed to be an account ID.
See also O(account_id) for a dedicated assignee account ID parameter.
Added in community.general 12.6.0.
- Jira Server and Jira Data Center may still accept O(assignee) as a username.
- Note that JIRA may not allow changing field values on specific transitions or states.
account_id:
type: str
@@ -161,6 +165,9 @@ options:
- This is a free-form data structure that can contain arbitrary data. This is passed directly to the JIRA REST API (possibly
after merging with other required data, as when passed to create). See examples for more information, and the JIRA
REST API for the structure required for various fields.
- When O(cloud=true), user-type fields (V(assignee), V(reporter), and any fields listed in O(custom_user_fields)) that
contain a string value with C(@) are automatically resolved from email to account ID. String values without C(@) are
assumed to be account IDs. Added in community.general 12.6.0.
- When passed to comment, the data structure is merged at the first level since community.general 4.6.0. Useful to add
JIRA properties for example.
- Note that JIRA may not allow changing field values on specific transitions or states.
@@ -193,16 +200,25 @@ options:
cloud:
description:
- Enable when using Jira Cloud.
- When set to V(true), O(operation=search) uses the C(/rest/api/2/search/jql) endpoint required by Jira Cloud,
since the legacy C(/rest/api/2/search) endpoint has been removed.
- When set to V(false) (the default), the legacy endpoint is used, which is still required for Jira Data Center / Server.
- See U(https://developer.atlassian.com/changelog/#CHANGE-2046) for details about the endpoint deprecation.
- In the future, if this option is set to V(true), other endpoints might also be replaced to address
Jira Cloud deprecations.
- When set to V(true), the module uses Jira Cloud compatible behavior.
- When set to V(false) (the default), the module uses Jira Server / Data Center compatible behavior.
- In the future, this option might affect additional operations for Jira Cloud compatibility.
type: bool
default: false
version_added: '12.6.0'
custom_user_fields:
description:
- A list of field names (for example V(customfield_10050)) that hold user values.
- When O(cloud=true) and a listed field is present in O(fields) as a string containing C(@), the module resolves the
email to a Jira Cloud account ID automatically.
- The built-in user fields O(assignee) and V(reporter) are always resolved when present; list here any further
user-typed fields (from other Jira products or extensions) that should receive the same resolution.
type: list
elements: str
default: []
version_added: '12.6.0'
attachment:
type: dict
version_added: 2.5.0
@@ -352,6 +368,39 @@ EXAMPLES = r"""
issuetype: Task
assignee: ssmith
# Assign an issue on Jira Cloud using an email address
# (the module resolves the email to an account ID automatically)
- name: Assign an issue on Jira Cloud using email
community.general.jira:
uri: '{{ server }}'
username: '{{ user }}'
password: '{{ pass }}'
issue: '{{ issue.meta.key }}'
operation: edit
cloud: true
assignee: user@example.com
# Create an issue on Jira Cloud with reporter and a custom user field
# resolved from email addresses to account IDs
- name: Create an issue on Jira Cloud with user field resolution
community.general.jira:
uri: '{{ server }}'
username: '{{ user }}'
password: '{{ pass }}'
project: ANS
operation: create
summary: Example Issue
description: Created using Ansible
issuetype: Task
cloud: true
assignee: assignee@example.com
custom_user_fields:
- customfield_10050
args:
fields:
reporter: reporter@example.com
customfield_10050: approver@example.com
# Edit an issue
- name: Set the labels on an issue using free-form fields
community.general.jira:
@@ -495,6 +544,7 @@ import os
import random
import string
import traceback
from urllib.parse import quote
from urllib.request import pathname2url
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
@@ -504,6 +554,8 @@ from ansible_collections.community.general.plugins.module_utils.module_helper im
class JIRA(StateModuleHelper):
USER_FIELDS = ["assignee", "reporter"]
module = dict(
argument_spec=dict(
attachment=dict(
@@ -581,6 +633,7 @@ class JIRA(StateModuleHelper):
type="str",
),
cloud=dict(type="bool", default=False),
custom_user_fields=dict(type="list", elements="str", default=[]),
maxresults=dict(type="int"),
timeout=dict(type="float", default=10),
validate_certs=dict(default=True, type="bool"),
@@ -612,14 +665,56 @@ class JIRA(StateModuleHelper):
state_param = "operation"
def __init_module__(self):
self.vars.uri = self.vars.uri.strip("/")
self.vars.set("restbase", f"{self.vars.uri}/rest/api/2")
if self.vars.fields is None:
self.vars.fields = {}
if self.vars.assignee:
self.vars.fields["assignee"] = {"name": self.vars.assignee}
if self.vars.cloud:
self.vars.fields["assignee"] = self.vars.assignee
else:
self.vars.fields["assignee"] = {"name": self.vars.assignee}
if self.vars.account_id:
self.vars.fields["assignee"] = {"accountId": self.vars.account_id}
self.vars.uri = self.vars.uri.strip("/")
self.vars.set("restbase", f"{self.vars.uri}/rest/api/2")
self.resolve_user_fields()
def resolve_user_fields(self):
if not self.vars.cloud or not self.vars.fields:
return
user_fields = self.USER_FIELDS + self.vars.custom_user_fields
for field_name in user_fields:
value = self.vars.fields.get(field_name)
if not isinstance(value, str):
continue
if "@" in value:
account_id = self.resolve_account_id(value, field_name)
self.vars.fields[field_name] = {"accountId": account_id}
else:
self.vars.fields[field_name] = {"accountId": value}
def resolve_account_id(self, email, field_name="assignee"):
url = f"{self.vars.restbase}/user/search?query={quote(email, safe='')}"
result = self.get(url)
if not isinstance(result, list) or len(result) != 1:
count = len(result) if isinstance(result, list) else 0
self.module.fail_json(
msg=(
f'Failed to resolve field "{field_name}" email "{email}" to a unique '
f'Jira Cloud account ID: found "{count}" result(s). '
"Specify the account ID directly."
)
)
user = result[0]
user_email = user.get("emailAddress")
if (user_email or "").lower() != email.lower():
self.module.fail_json(
msg=(
f'Failed to resolve field "{field_name}" email "{email}": '
f'the email address on the matched account "{user_email}" does not match. '
"Specify the account ID directly."
)
)
return user["accountId"]
@cause_changes(when="success")
def operation_create(self):