From be4cf3ba4dd9f6d4dc2b41686b5c9b662396c4b8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:36:05 +0200 Subject: [PATCH] [PR #11735/3e9689b1 backport][stable-12] jira - resolve Cloud assignee email to account ID via user search (#11891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 3e9689b13d65ba56d1430223d124f0276617fca5) Signed-off-by: Vladimir Vasilev Co-authored-by: vladi-k <53343355+vladi-k@users.noreply.github.com> --- ...4-jira-cloud-assignee-email-resolution.yml | 7 ++ plugins/modules/jira.py | 115 ++++++++++++++++-- 2 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/11734-jira-cloud-assignee-email-resolution.yml diff --git a/changelogs/fragments/11734-jira-cloud-assignee-email-resolution.yml b/changelogs/fragments/11734-jira-cloud-assignee-email-resolution.yml new file mode 100644 index 0000000000..f2da487ba1 --- /dev/null +++ b/changelogs/fragments/11734-jira-cloud-assignee-email-resolution.yml @@ -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). diff --git a/plugins/modules/jira.py b/plugins/modules/jira.py index f632d24030..bb65719c53 100644 --- a/plugins/modules/jira.py +++ b/plugins/modules/jira.py @@ -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):