11
22import asyncio
33import logging
4+ from urllib .parse import quote
45
56import aiohttp
67import urllib3
@@ -31,26 +32,47 @@ def __init__(
3132 self .username = username
3233 self .ticket_queue = ticket_queue
3334 self .password = password
35+ self .token = token
36+ self ._auth = None
3437 if not loop :
3538 self .loop = asyncio .new_event_loop ()
3639 self .new_loop = True
3740 else :
3841 self .loop = loop
3942 self .new_loop = False
40- self .token = token
41- if not self .token :
42- if self .password :
43- payload = BasicAuth (self .username , self .password )
44- else :
43+
44+ # Jira Cloud API tokens use HTTP Basic auth (email + token), same as curl -u user:token
45+ if self .token :
46+ if not self .username :
4547 logger .error (
46- "Basic Authentication expected as no token was found but password is missing "
48+ "Basic Authentication with API token requires JIRA_USERNAME (email) "
4749 )
48- raise JiraException
50+ raise JiraException (
51+ "Basic Authentication with API token requires JIRA_USERNAME (email)"
52+ )
53+ self ._auth = BasicAuth (self .username , self .token )
54+ elif self .password :
55+ if not self .username :
56+ logger .error (
57+ "Basic Authentication requires JIRA_USERNAME"
58+ )
59+ raise JiraException (
60+ "Basic Authentication requires JIRA_USERNAME"
61+ )
62+ self ._auth = BasicAuth (self .username , self .password )
4963 else :
50- payload = "Bearer: %s" % self .token
51- self .headers = {"Authorization" : payload }
52- except :
53- pass
64+ logger .error (
65+ "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD"
66+ )
67+ raise JiraException (
68+ "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD"
69+ )
70+ self .headers = {"Accept" : "application/json" }
71+ except JiraException :
72+ raise
73+ except Exception as ex :
74+ logger .error ("Failed to initialize Jira client: %s" , ex )
75+ raise JiraException from ex
5476
5577 def __exit__ (self ):
5678 if self .new_loop :
@@ -60,6 +82,7 @@ async def get_request(self, endpoint):
6082 logger .debug ("GET: %s" % endpoint )
6183 try :
6284 async with aiohttp .ClientSession (
85+ auth = self ._auth ,
6386 headers = self .headers ,
6487 loop = self .loop ,
6588 ) as session :
@@ -81,14 +104,19 @@ async def post_request(self, endpoint, payload):
81104 logger .debug ("POST: {%s:%s}" % (endpoint , payload ))
82105 try :
83106 async with aiohttp .ClientSession (
84- headers = self .headers , loop = self .loop
107+ auth = self ._auth ,
108+ headers = self .headers ,
109+ loop = self .loop ,
85110 ) as session :
86111 async with session .post (
87112 self .url + endpoint ,
88113 json = payload ,
89114 verify_ssl = False ,
90115 ) as response :
91- data = await response .json (content_type = "application/json" )
116+ if response .status == 204 :
117+ data = {}
118+ else :
119+ data = await response .json (content_type = "application/json" )
92120 except Exception as ex :
93121 logger .debug (ex )
94122 logger .error ("There was something wrong with your request." )
@@ -98,13 +126,17 @@ async def post_request(self, endpoint, payload):
98126 return data
99127 if response .status == 404 :
100128 logger .error ("Resource not found: %s" % self .url + endpoint )
129+ else :
130+ logger .error (data )
101131 return False
102132
103133 async def put_request (self , endpoint , payload ):
104134 logger .debug ("POST: {%s:%s}" % (endpoint , payload ))
105135 try :
106136 async with aiohttp .ClientSession (
107- headers = self .headers , loop = self .loop
137+ auth = self ._auth ,
138+ headers = self .headers ,
139+ loop = self .loop ,
108140 ) as session :
109141 async with session .put (
110142 self .url + endpoint ,
@@ -158,11 +190,37 @@ async def create_subtask(self, parent_ticket, cloud, description, type_of_subtas
158190 response = await self .post_request (endpoint , data )
159191 return response
160192
193+ async def _resolve_watcher_to_account_id (self , watcher ):
194+ """
195+ Jira Cloud expects an Atlassian accountId in the watchers POST body (JSON string),
196+ not an email. Resolve email or short username via REST v2 user search.
197+ """
198+ if not watcher or not str (watcher ).strip ():
199+ return None
200+ w = str (watcher ).strip ()
201+ user = await self .get_user_by_email (w )
202+ if user and user .get ("accountId" ):
203+ return user ["accountId" ]
204+ if "@" not in w :
205+ user = await self .get_user_by_email (f"{ w } @redhat.com" )
206+ if user and user .get ("accountId" ):
207+ return user ["accountId" ]
208+ logger .error ("Could not resolve Jira accountId for watcher: %s" , w )
209+ return None
210+
161211 async def add_watcher (self , ticket , watcher ):
212+ account_id = await self ._resolve_watcher_to_account_id (watcher )
213+ if not account_id :
214+ return False
162215 issue_id = "%s-%s" % (self .ticket_queue , ticket )
163216 endpoint = "/issue/%s/watchers" % issue_id
164- logger .debug ("POST transition: {%s:%s}" % (issue_id , watcher ))
165- response = await self .post_request (endpoint , watcher )
217+ logger .debug (
218+ "POST watcher issue=%s accountId=%s (from %s)" ,
219+ issue_id ,
220+ account_id ,
221+ watcher ,
222+ )
223+ response = await self .post_request (endpoint , account_id )
166224 return response
167225
168226 async def add_label (self , ticket , label ):
@@ -221,15 +279,18 @@ async def get_watchers(self, ticket):
221279 return result
222280
223281 async def get_user_by_email (self , email ):
224- endpoint = f"/user/search?username= { email } "
282+ endpoint = f"/user/search?query= { quote ( email ) } "
225283 logger .debug ("GET user: %s" % endpoint )
226284 result = await self .get_request (endpoint )
227285 if not result :
228- logger .error ("User not found" )
286+ logger .error ("User not found for query: %s" , email )
229287 return None
288+ el = email .lower ()
230289 for user in result :
231- if user .get ("emailAddress" ) == email :
290+ if ( user .get ("emailAddress" ) or "" ). lower () == el :
232291 return user
292+ if len (result ) == 1 :
293+ return result [0 ]
233294 return None
234295
235296 async def get_pending_tickets (self ):
0 commit comments