From 752349b16a28b6642520076a685f8c26f5c224eb Mon Sep 17 00:00:00 2001 From: pragya811 Date: Thu, 26 Mar 2026 12:57:51 +0530 Subject: [PATCH 1/4] Fix build issue during quay uploads (#980) --- .bumpversion.cfg | 2 +- Dockerfile | 6 ++++-- setup.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1d701543..7097b21a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = False tag = True -current_version = 1.1.412 +current_version = 1.1.413 tag_name = v{current_version} message = GitHub Actions Build {current_version} diff --git a/Dockerfile b/Dockerfile index c6abb5c3..33545f81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,10 @@ RUN apt-get update \ && mv gitleaks /usr/local/bin/ \ && gitleaks --version -# install & run cloud-governance (--no-cache-dir for take always the latest) -RUN python -m pip --no-cache-dir install --upgrade pip && pip --no-cache-dir install cloud-governance --upgrade +# Pin setuptools<82 so pkg_resources is available when building ibm-cloud-sdk-core +RUN python -m pip --no-cache-dir install --upgrade pip "setuptools>=58,<82" wheel && \ + pip --no-cache-dir install --no-build-isolation ibm-cloud-sdk-core==3.18.0 ibm-platform-services==0.27.0 ibm-vpc==0.21.0 ibm-schematics==1.0.1 && \ + pip --no-cache-dir install cloud-governance --upgrade RUN pip3 --no-cache-dir install --upgrade awscli ADD cloud_governance/policy /usr/local/cloud_governance/policy/ COPY cloud_governance/main/main.py /usr/local/cloud_governance/main.py diff --git a/setup.py b/setup.py index bf81f69c..22d43a0d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from os import path from setuptools import setup, find_packages -__version__ = '1.1.412' +__version__ = '1.1.413' here = path.abspath(path.dirname(__file__)) From 1cc98ecf00dc09e0611b78bdf0a111ef689e7df1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:23:22 +0000 Subject: [PATCH 2/4] bump version to exist version v1.1.413 --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7097b21a..cb652510 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = False tag = True -current_version = 1.1.413 +current_version = 1.1.414 tag_name = v{current_version} message = GitHub Actions Build {current_version} diff --git a/setup.py b/setup.py index 22d43a0d..1f7f36a7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from os import path from setuptools import setup, find_packages -__version__ = '1.1.413' +__version__ = '1.1.414' here = path.abspath(path.dirname(__file__)) From f06bbc87c1cb53e98bd5116fafbf8c8498c24dd8 Mon Sep 17 00:00:00 2001 From: pragya811 Date: Thu, 26 Mar 2026 13:56:31 +0530 Subject: [PATCH 3/4] Handle JIRA Auth changes with Atlassian Cloud Migration (#977) * Test JIRA auth changes * Test JIRA auth changes * Test JIRA auth changes * Unittest changes --- cloud_governance/common/jira/jira.py | 99 +++++++++++++++---- .../common/jira/jira_operations.py | 10 +- .../mocks/mock_jira.py | 29 ++++-- 3 files changed, 112 insertions(+), 26 deletions(-) diff --git a/cloud_governance/common/jira/jira.py b/cloud_governance/common/jira/jira.py index 81fe5ee2..181606a8 100644 --- a/cloud_governance/common/jira/jira.py +++ b/cloud_governance/common/jira/jira.py @@ -1,6 +1,7 @@ import asyncio import logging +from urllib.parse import quote import aiohttp import urllib3 @@ -31,26 +32,47 @@ def __init__( self.username = username self.ticket_queue = ticket_queue self.password = password + self.token = token + self._auth = None if not loop: self.loop = asyncio.new_event_loop() self.new_loop = True else: self.loop = loop self.new_loop = False - self.token = token - if not self.token: - if self.password: - payload = BasicAuth(self.username, self.password) - else: + + # Jira Cloud API tokens use HTTP Basic auth (email + token), same as curl -u user:token + if self.token: + if not self.username: logger.error( - "Basic Authentication expected as no token was found but password is missing" + "Basic Authentication with API token requires JIRA_USERNAME (email)" ) - raise JiraException + raise JiraException( + "Basic Authentication with API token requires JIRA_USERNAME (email)" + ) + self._auth = BasicAuth(self.username, self.token) + elif self.password: + if not self.username: + logger.error( + "Basic Authentication requires JIRA_USERNAME" + ) + raise JiraException( + "Basic Authentication requires JIRA_USERNAME" + ) + self._auth = BasicAuth(self.username, self.password) else: - payload = "Bearer: %s" % self.token - self.headers = {"Authorization": payload} - except: - pass + logger.error( + "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD" + ) + raise JiraException( + "Basic Authentication expected: set JIRA_TOKEN or JIRA_PASSWORD" + ) + self.headers = {"Accept": "application/json"} + except JiraException: + raise + except Exception as ex: + logger.error("Failed to initialize Jira client: %s", ex) + raise JiraException from ex def __exit__(self): if self.new_loop: @@ -60,6 +82,7 @@ async def get_request(self, endpoint): logger.debug("GET: %s" % endpoint) try: async with aiohttp.ClientSession( + auth=self._auth, headers=self.headers, loop=self.loop, ) as session: @@ -81,14 +104,19 @@ async def post_request(self, endpoint, payload): logger.debug("POST: {%s:%s}" % (endpoint, payload)) try: async with aiohttp.ClientSession( - headers=self.headers, loop=self.loop + auth=self._auth, + headers=self.headers, + loop=self.loop, ) as session: async with session.post( self.url + endpoint, json=payload, verify_ssl=False, ) as response: - data = await response.json(content_type="application/json") + if response.status == 204: + data = {} + else: + data = await response.json(content_type="application/json") except Exception as ex: logger.debug(ex) logger.error("There was something wrong with your request.") @@ -98,13 +126,17 @@ async def post_request(self, endpoint, payload): return data if response.status == 404: logger.error("Resource not found: %s" % self.url + endpoint) + else: + logger.error(data) return False async def put_request(self, endpoint, payload): logger.debug("POST: {%s:%s}" % (endpoint, payload)) try: async with aiohttp.ClientSession( - headers=self.headers, loop=self.loop + auth=self._auth, + headers=self.headers, + loop=self.loop, ) as session: async with session.put( self.url + endpoint, @@ -158,11 +190,37 @@ async def create_subtask(self, parent_ticket, cloud, description, type_of_subtas response = await self.post_request(endpoint, data) return response + async def _resolve_watcher_to_account_id(self, watcher): + """ + Jira Cloud expects an Atlassian accountId in the watchers POST body (JSON string), + not an email. Resolve email or short username via REST v2 user search. + """ + if not watcher or not str(watcher).strip(): + return None + w = str(watcher).strip() + user = await self.get_user_by_email(w) + if user and user.get("accountId"): + return user["accountId"] + if "@" not in w: + user = await self.get_user_by_email(f"{w}@redhat.com") + if user and user.get("accountId"): + return user["accountId"] + logger.error("Could not resolve Jira accountId for watcher: %s", w) + return None + async def add_watcher(self, ticket, watcher): + account_id = await self._resolve_watcher_to_account_id(watcher) + if not account_id: + return False issue_id = "%s-%s" % (self.ticket_queue, ticket) endpoint = "/issue/%s/watchers" % issue_id - logger.debug("POST transition: {%s:%s}" % (issue_id, watcher)) - response = await self.post_request(endpoint, watcher) + logger.debug( + "POST watcher issue=%s accountId=%s (from %s)", + issue_id, + account_id, + watcher, + ) + response = await self.post_request(endpoint, account_id) return response async def add_label(self, ticket, label): @@ -221,15 +279,18 @@ async def get_watchers(self, ticket): return result async def get_user_by_email(self, email): - endpoint = f"/user/search?username={email}" + endpoint = f"/user/search?query={quote(email)}" logger.debug("GET user: %s" % endpoint) result = await self.get_request(endpoint) if not result: - logger.error("User not found") + logger.error("User not found for query: %s", email) return None + el = email.lower() for user in result: - if user.get("emailAddress") == email: + if (user.get("emailAddress") or "").lower() == el: return user + if len(result) == 1: + return result[0] return None async def get_pending_tickets(self): diff --git a/cloud_governance/common/jira/jira_operations.py b/cloud_governance/common/jira/jira_operations.py index 99442d6b..93c3e6f7 100644 --- a/cloud_governance/common/jira/jira_operations.py +++ b/cloud_governance/common/jira/jira_operations.py @@ -31,10 +31,18 @@ def __init__(self): self.__jira_url = self.__environment_variables_dict.get('JIRA_URL').strip() self.__jira_username = self.__environment_variables_dict.get('JIRA_USERNAME').strip() self.__jira_token = self.__environment_variables_dict.get('JIRA_TOKEN').strip() + self.__jira_password = self.__environment_variables_dict.get('JIRA_PASSWORD', '').strip() self.__jira_queue = self.__environment_variables_dict.get('JIRA_QUEUE').strip() self.__cache_temp_dir = self.__environment_variables_dict.get('TEMPORARY_DIRECTORY', '').strip() self.__loop = asyncio.new_event_loop() - self.__jira_object = Jira(url=self.__jira_url, username=self.__jira_username, token=self.__jira_token, ticket_queue=self.__jira_queue, loop=self.__loop) + self.__jira_object = Jira( + url=self.__jira_url, + username=self.__jira_username, + password=self.__jira_password or None, + token=self.__jira_token or None, + ticket_queue=self.__jira_queue, + loop=self.__loop, + ) @typeguard.typechecked @logger_time_stamp diff --git a/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py b/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py index 11032229..1f414c70 100644 --- a/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py +++ b/tests/unittest/cloud_governance/cloud_resource_orchestration/mocks/mock_jira.py @@ -2,8 +2,16 @@ from functools import wraps from unittest.mock import patch - from cloud_governance.common.jira.jira_operations import JiraOperations +from cloud_governance.main.environment_variables import environment_variables + +# Jira() is still constructed with real credentials; CI often has no JIRA_* env vars. +_JIRA_STANDIN_KEYS = ('JIRA_USERNAME', 'JIRA_URL', 'JIRA_QUEUE') +_JIRA_STANDIN_DEFAULTS = { + 'JIRA_USERNAME': 'mock@example.com', + 'JIRA_URL': 'https://mock.atlassian.net', + 'JIRA_QUEUE': 'MOCK', +} def get_ticket_response(): @@ -94,10 +102,19 @@ def method_wrapper(*args, **kwargs): @param kwargs: @return: """ - with patch.object(JiraOperations, 'get_issue', mock_get_issue),\ - patch.object(JiraOperations, 'move_issue_state', mock_move_issue_state), \ - patch.object(JiraOperations, 'get_all_issues', mock_get_all_issues): - result = method(*args, **kwargs) - return result + env = environment_variables.environment_variables_dict + saved_jira = {k: env.get(k, '') for k in _JIRA_STANDIN_KEYS} + try: + for key, default in _JIRA_STANDIN_DEFAULTS.items(): + if not (env.get(key) or '').strip(): + env[key] = default + with patch.object(JiraOperations, 'get_issue', mock_get_issue),\ + patch.object(JiraOperations, 'move_issue_state', mock_move_issue_state), \ + patch.object(JiraOperations, 'get_all_issues', mock_get_all_issues): + result = method(*args, **kwargs) + return result + finally: + for key in _JIRA_STANDIN_KEYS: + env[key] = saved_jira[key] return method_wrapper From c2e62c5524f114f61ccc74cbec93ab0f27d29b9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:27:17 +0000 Subject: [PATCH 4/4] bump version to exist version v1.1.414 --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cb652510..7206a0de 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = False tag = True -current_version = 1.1.414 +current_version = 1.1.415 tag_name = v{current_version} message = GitHub Actions Build {current_version} diff --git a/setup.py b/setup.py index 1f7f36a7..8903df58 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from os import path from setuptools import setup, find_packages -__version__ = '1.1.414' +__version__ = '1.1.415' here = path.abspath(path.dirname(__file__))