From 3e4cd04d88444ab763a3d119f17a0c0d3fead8c6 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:01:21 +0100 Subject: [PATCH 01/15] initial attempt of a Jira Cloud github action --- .github/workflows/jira_cloud_ci.yml | 64 +++++++++++++++++++++++++++++ tests/conftest.py | 58 ++++---------------------- 2 files changed, 72 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/jira_cloud_ci.yml diff --git a/.github/workflows/jira_cloud_ci.yml b/.github/workflows/jira_cloud_ci.yml new file mode 100644 index 000000000..a903c824d --- /dev/null +++ b/.github/workflows/jira_cloud_ci.yml @@ -0,0 +1,64 @@ +name: Jira Cloud CI + +on: + workflow_run: + workflows: ["Jira Server CI"] + types: + - completed + +jobs: + test: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + name: ${{ matrix.os }} / Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest + strategy: + matrix: + os: [Ubuntu] + # We only test a single version to prevent concurrent + # running of tests influencing one another + python-version: [3.8] + + steps: + - uses: actions/checkout@master + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Setup the Pip cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: >- + ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('tox.ini') }}-${{ + hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install Dependencies + run: | + sudo apt-get update; sudo apt-get install gcc libkrb5-dev + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Test with tox + run: tox + env: + CI_JIRA_TYPE: CLOUD + CI_JIRA_CLOUD_ADMIN: ${{ secrets.CI_JIRA_CLOUD_ADMIN }} + CI_JIRA_CLOUD_ADMIN_TOKEN: ${{ secrets.CI_JIRA_CLOUD_ADMIN_TOKEN }} + CI_JIRA_CLOUD_USER: ${{ secrets.CI_JIRA_CLOUD_USER }} + CI_JIRA_CLOUD_USER_TOKEN: ${{ secrets.CI_JIRA_CLOUD_USER_TOKEN }} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.15 + with: + file: ./coverage.xml + name: ${{ runner.os }}-${{ matrix.python-version }}-Cloud diff --git a/tests/conftest.py b/tests/conftest.py index 26a1d2ca8..25bc9893a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,18 +21,6 @@ LOGGER = logging.getLogger(__name__) -OAUTH = False -CONSUMER_KEY = "oauth-consumer" -KEY_CERT_FILE = "/home/bspeakmon/src/atlassian-oauth-examples/rsa.pem" -KEY_CERT_DATA = None -try: - with open(KEY_CERT_FILE, "r") as cert: - KEY_CERT_DATA = cert.read() - OAUTH = True -except Exception: - OAUTH = False - - ON_CUSTOM_JIRA = "CI_JIRA_URL" in os.environ @@ -124,7 +112,7 @@ class JiraTestManager(object): __shared_state: Dict[Any, Any] = {} - def __init__(self, jira_hosted_type="Server"): + def __init__(self, jira_hosted_type=os.environ.get("CI_JIRA_TYPE", "Server")): """Instantiate and populate the JIRA instance""" self.__dict__ = self.__shared_state @@ -132,7 +120,7 @@ def __init__(self, jira_hosted_type="Server"): self.initialized = False self.max_retries = 5 - if jira_hosted_type and jira_hosted_type == "Cloud": + if jira_hosted_type and jira_hosted_type.upper() == "CLOUD": self.set_jira_cloud_details() else: self.set_jira_server_details() @@ -143,10 +131,8 @@ def __init__(self, jira_hosted_type="Server"): "validate": True, "max_retries": self.max_retries, } - if OAUTH: - self.set_oauth_logins() - else: - self.set_basic_auth_logins(**jira_class_kwargs) + + self.set_basic_auth_logins(**jira_class_kwargs) if not self.jira_admin.current_user(): self.initialized = True @@ -164,10 +150,10 @@ def __init__(self, jira_hosted_type="Server"): def set_jira_cloud_details(self): self.CI_JIRA_URL = "https://pycontribs.atlassian.net" - self.CI_JIRA_ADMIN = "ci-admin" - self.CI_JIRA_ADMIN_PASSWORD = "sd4s3dgec5fhg4tfsds3434" - self.CI_JIRA_USER = "ci-user" - self.CI_JIRA_USER_PASSWORD = "sd4s3dgec5fhg4tfsds3434" + self.CI_JIRA_ADMIN = os.environ["CI_JIRA_CLOUD_ADMIN"] + self.CI_JIRA_ADMIN_PASSWORD = os.environ["CI_JIRA_CLOUD_ADMIN_TOKEN"] + self.CI_JIRA_USER = os.environ["CI_JIRA_CLOUD_USER"] + self.CI_JIRA_USER_PASSWORD = os.environ["CI_JIRA_CLOUD_USER_TOKEN"] def set_jira_server_details(self): self.CI_JIRA_URL = os.environ["CI_JIRA_URL"] @@ -177,34 +163,6 @@ def set_jira_server_details(self): self.CI_JIRA_USER_PASSWORD = os.environ["CI_JIRA_USER_PASSWORD"] self.CI_JIRA_ISSUE = os.environ.get("CI_JIRA_ISSUE", "Bug") - def set_oauth_logins(self): - self.jira_admin = JIRA( - oauth={ - "access_token": "hTxcwsbUQiFuFALf7KZHDaeAJIo3tLUK", - "access_token_secret": "aNCLQFP3ORNU6WY7HQISbqbhf0UudDAf", - "consumer_key": CONSUMER_KEY, - "key_cert": KEY_CERT_DATA, - } - ) - self.jira_sysadmin = JIRA( - oauth={ - "access_token": "4ul1ETSFo7ybbIxAxzyRal39cTrwEGFv", - "access_token_secret": "K83jBZnjnuVRcfjBflrKyThJa0KSjSs2", - "consumer_key": CONSUMER_KEY, - "key_cert": KEY_CERT_DATA, - }, - logging=False, - max_retries=self.max_retries, - ) - self.jira_normal = JIRA( - oauth={ - "access_token": "ZVDgYDyIQqJY8IFlQ446jZaURIz5ECiB", - "access_token_secret": "5WbLBybPDg1lqqyFjyXSCsCtAWTwz1eD", - "consumer_key": CONSUMER_KEY, - "key_cert": KEY_CERT_DATA, - } - ) - def set_basic_auth_logins(self, **jira_class_kwargs): if self.CI_JIRA_ADMIN: self.jira_admin = JIRA( From ddbc0ccba5ee29711985e11e9c6552025112ab3f Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Tue, 27 Jul 2021 22:58:15 +0100 Subject: [PATCH 02/15] add opt-in marker for running tests on jira cloud --- .github/workflows/jira_cloud_ci.yml | 2 +- make_local_jira_user.py | 5 +++++ setup.cfg | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jira_cloud_ci.yml b/.github/workflows/jira_cloud_ci.yml index a903c824d..0625e373b 100644 --- a/.github/workflows/jira_cloud_ci.yml +++ b/.github/workflows/jira_cloud_ci.yml @@ -49,7 +49,7 @@ jobs: python -m pip install --upgrade tox tox-gh-actions - name: Test with tox - run: tox + run: tox -e py38 -- -m allow_on_cloud env: CI_JIRA_TYPE: CLOUD CI_JIRA_CLOUD_ADMIN: ${{ secrets.CI_JIRA_CLOUD_ADMIN }} diff --git a/make_local_jira_user.py b/make_local_jira_user.py index 5c9d8897b..9452a5ab2 100644 --- a/make_local_jira_user.py +++ b/make_local_jira_user.py @@ -1,6 +1,7 @@ """Attempts to create a test user, as the empty JIRA instance isn't provisioned with one. """ +import sys import time from os import environ @@ -29,6 +30,10 @@ def add_user_to_jira(): if __name__ == "__main__": + if environ.get("CI_JIRA_TYPE", "Server").upper() == "CLOUD": + print("Do not need to create a user for Jira Cloud CI, quitting.") + sys.exit() + start_time = time.time() timeout_mins = 15 print( diff --git a/setup.cfg b/setup.cfg index 600285b78..98345feac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -160,5 +160,8 @@ timeout = 80 filterwarnings = ignore::pytest.PytestWarning +markers = + allow_on_cloud: opt in for the test to run on Jira Cloud + [mypy] python_version = 3.6 From 5ce570bf093cebdc9f81c6f2d73812a27229f300 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Wed, 28 Jul 2021 00:04:10 +0100 Subject: [PATCH 03/15] fixes to allow Cloud tests to run update error handling in JiraTestManager, remove default flaky --- jira/client.py | 9 ++- tests/conftest.py | 163 +++++++++++++++++++++++++++------------------- 2 files changed, 103 insertions(+), 69 deletions(-) diff --git a/jira/client.py b/jira/client.py index 12a069fab..d9aec95e5 100644 --- a/jira/client.py +++ b/jira/client.py @@ -3731,10 +3731,14 @@ def backup_download(self, filename: str = None): self.log.error(ioe) return None - def current_user(self, field: str = "key") -> str: + def current_user(self, field: Optional[str] = None) -> str: """Returns the username or emailAddress of the current user. For anonymous users it will return a value that evaluates as False. + Args: + field (Optional[str]): the name of the identifier field. + Defaults to "accountId" for Jira Cloud, else "key" + Returns: str """ @@ -3746,6 +3750,9 @@ def current_user(self, field: str = "key") -> str: r_json: Dict[str, str] = json_loads(r) self._myself = r_json + if field is None: + field = "accountId" if self._is_cloud else "key" + return self._myself[field] def delete_project(self, pid: Union[str, Project]) -> Optional[bool]: diff --git a/tests/conftest.py b/tests/conftest.py index 25bc9893a..eff401e79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,6 @@ from typing import Any, Dict import pytest -from flaky import flaky from jira import JIRA @@ -21,20 +20,11 @@ LOGGER = logging.getLogger(__name__) -ON_CUSTOM_JIRA = "CI_JIRA_URL" in os.environ - - -not_on_custom_jira_instance = pytest.mark.skipif( - ON_CUSTOM_JIRA, reason="Not applicable for custom Jira instance" -) -if ON_CUSTOM_JIRA: - LOGGER.info("Picked up custom Jira engine.") - +allow_on_cloud = pytest.mark.allow_on_cloud broken_test = pytest.mark.xfail -@flaky # all have default flaki-ness class JiraTestCase(unittest.TestCase): """Test case for all Jira tests. @@ -61,7 +51,17 @@ def setUp(self) -> None: This is called before each test. If you want to add more for your tests, Run `JiraTestCase.setUp(self) in your custom setUp() to obtain these. """ - self.test_manager = JiraTestManager() + + initialized = False + try: + self.test_manager = JiraTestManager() + initialized = self.test_manager.initialized + except Exception as e: + # pytest with flaky swallows any exceptions re-raised in a try, except + # so we log any exceptions for aiding debugging + LOGGER.exception(e) + self.assertTrue(initialized, "Test Manager setUp failed") + self.jira = self.test_manager.jira_admin self.jira_normal = self.test_manager.jira_normal self.user_admin = self.test_manager.user_admin @@ -108,6 +108,7 @@ class JiraTestManager(object): CI_JIRA_ADMIN (str): Admin user account name. CI_JIRA_USER (str): Limited user account name. max_retries (int): number of retries to perform for recoverable HTTP errors. + initialized (bool): if init was successful. """ __shared_state: Dict[Any, Any] = {} @@ -119,9 +120,11 @@ def __init__(self, jira_hosted_type=os.environ.get("CI_JIRA_TYPE", "Server")): if not self.__dict__: self.initialized = False self.max_retries = 5 + self._cloud_ci = False if jira_hosted_type and jira_hosted_type.upper() == "CLOUD": self.set_jira_cloud_details() + self._cloud_ci = True else: self.set_jira_server_details() @@ -144,8 +147,12 @@ def __init__(self, jira_hosted_type=os.environ.get("CI_JIRA_TYPE", "Server")): if not hasattr(self, "jira_normal") or not hasattr(self, "jira_admin"): pytest.exit("FATAL: WTF!?") - self.user_admin = self.jira_admin.search_users(self.CI_JIRA_ADMIN)[0] - self.user_normal = self.jira_admin.search_users(self.CI_JIRA_USER)[0] + if self._cloud_ci: + self.user_admin = self.jira_admin.search_users(query=self.CI_JIRA_ADMIN)[0] + self.user_normal = self.jira_admin.search_users(query=self.CI_JIRA_USER)[0] + else: + self.user_admin = self.jira_admin.search_users(self.CI_JIRA_ADMIN)[0] + self.user_normal = self.jira_admin.search_users(self.CI_JIRA_USER)[0] self.initialized = True def set_jira_cloud_details(self): @@ -154,6 +161,7 @@ def set_jira_cloud_details(self): self.CI_JIRA_ADMIN_PASSWORD = os.environ["CI_JIRA_CLOUD_ADMIN_TOKEN"] self.CI_JIRA_USER = os.environ["CI_JIRA_CLOUD_USER"] self.CI_JIRA_USER_PASSWORD = os.environ["CI_JIRA_CLOUD_USER_TOKEN"] + self.CI_JIRA_ISSUE = os.environ.get("CI_JIRA_ISSUE", "Bug") def set_jira_server_details(self): self.CI_JIRA_URL = os.environ["CI_JIRA_URL"] @@ -183,6 +191,67 @@ def set_basic_auth_logins(self, **jira_class_kwargs): self.jira_sysadmin = JIRA(self.CI_JIRA_URL, **jira_class_kwargs) self.jira_normal = JIRA(self.CI_JIRA_URL, **jira_class_kwargs) + def _project_exists(self, project_key: str) -> bool: + try: + self.jira_admin.project(project_key) + except Exception as e: # If the project does not exist a warning is thrown + if "No project could be found" in str(e): + return False + return True + + def _remove_project(self, project_key): + """Ensure if the project exists we delete it first""" + + wait_between_checks_secs = 2 + time_to_wait_for_delete_secs = 40 + wait_attempts = int(time_to_wait_for_delete_secs / wait_between_checks_secs) + + # TODO(ssbarnea): find a way to prevent SecurityTokenMissing for On Demand + # https://jira.atlassian.com/browse/JRA-39153 + try: + self.jira_admin.project(project_key) + except Exception as e: # If the project does not exist a warning is thrown + if "No project could be found" not in str(e): + raise e + else: + # if no error is thrown that means the project exists, so we try to delete it + try: + self.jira_admin.delete_project(project_key) + except Exception as e: + LOGGER.warning("Failed to delete %s\n%s", project_key, e) + + # wait for the project to be deleted + for _ in range(1, wait_attempts): + if not self._project_exists(project_key): + # If the project does not exist a warning is thrown + # so once this is raised we know it is deleted successfully + break + sleep(wait_between_checks_secs) + + if self._project_exists(project_key): + raise TimeoutError( + " Project '{project_key}' not deleted after {time_to_wait_for_delete_secs} seconds" + ) + + def _create_project( + self, project_key: str, project_name: str, allow_exist: bool = False + ) -> int: + """Create a project and return the id""" + + if allow_exist and self._project_exists(project_key): + pass + else: + self._remove_project(project_key) + create_attempts = 6 + for _ in range(create_attempts): + try: + if self.jira_admin.create_project(project_key, project_name): + break + except Exception as e: + if "A project with that name already exists" not in str(e): + raise e + return self.jira_admin.project(project_key).id + def create_some_data(self): """Create some data for the tests""" @@ -217,72 +286,30 @@ def create_some_data(self): self.project_sd, ) - # TODO(ssbarnea): find a way to prevent SecurityTokenMissing for On Demand - # https://jira.atlassian.com/browse/JRA-39153 - try: - self.jira_admin.project(self.project_a) - except Exception as e: - LOGGER.warning(e) - else: - try: - self.jira_admin.delete_project(self.project_a) - except Exception as e: - LOGGER.warning("Failed to delete %s\n%s", self.project_a, e) - - try: - self.jira_admin.project(self.project_b) - except Exception as e: - LOGGER.warning(e) - else: - try: - self.jira_admin.delete_project(self.project_b) - except Exception as e: - LOGGER.warning("Failed to delete %s\n%s", self.project_b, e) - - # wait for the project to be deleted - for _ in range(1, 20): - try: - self.jira_admin.project(self.project_b) - except Exception: - break - print("Warning: Project not deleted yet....") - sleep(2) - - for _ in range(6): - try: - if self.jira_admin.create_project(self.project_a, self.project_a_name): - break - except Exception as e: - if "A project with that name already exists" not in str(e): - raise e - self.project_a_id = self.jira_admin.project(self.project_a).id - self.jira_admin.create_project(self.project_b, self.project_b_name) + self.project_a_id = self._create_project(self.project_a, self.project_a_name) + self.project_b_id = self._create_project( + self.project_b, self.project_b_name, allow_exist=True + ) - try: - self.jira_admin.create_project(self.project_b, self.project_b_name) - except Exception: - # we care only for the project to exist - pass sleep(1) # keep it here as often Jira will report the # project as missing even after is created + + project_b_issue_kwargs = { + "project": self.project_b, + "issuetype": {"name": self.CI_JIRA_ISSUE}, + } self.project_b_issue1_obj = self.jira_admin.create_issue( - project=self.project_b, - summary="issue 1 from %s" % self.project_b, - issuetype=self.CI_JIRA_ISSUE, + summary="issue 1 from %s" % self.project_b, **project_b_issue_kwargs ) self.project_b_issue1 = self.project_b_issue1_obj.key self.project_b_issue2_obj = self.jira_admin.create_issue( - project=self.project_b, - summary="issue 2 from %s" % self.project_b, - issuetype={"name": self.CI_JIRA_ISSUE}, + summary="issue 2 from %s" % self.project_b, **project_b_issue_kwargs ) self.project_b_issue2 = self.project_b_issue2_obj.key self.project_b_issue3_obj = self.jira_admin.create_issue( - project=self.project_b, - summary="issue 3 from %s" % self.project_b, - issuetype={"name": self.CI_JIRA_ISSUE}, + summary="issue 3 from %s" % self.project_b, **project_b_issue_kwargs ) self.project_b_issue3 = self.project_b_issue3_obj.key From cdb75f72e746e166031cde45596c7fb90994e16c Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Thu, 29 Jul 2021 19:08:09 +0100 Subject: [PATCH 04/15] PIP_CONSTRAINT env fix for Windows --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 491f8fe39..b49ac1c61 100644 --- a/tox.ini +++ b/tox.ini @@ -114,7 +114,7 @@ deps = pre-commit>=1.17.0 commands= python -m pre_commit run --color=always {posargs:--all} setenv = - PIP_CONSTRAINT=/dev/null + PIP_CONSTRAINT= skip_install = true usedevelop = false From 026319375306851d71f9c6323a46bd24801a9df6 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Thu, 29 Jul 2021 21:21:51 +0100 Subject: [PATCH 05/15] use property access to is_jira_cloud_ci --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index eff401e79..142219ccc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,6 +69,11 @@ def setUp(self) -> None: self.project_b = self.test_manager.project_b self.project_a = self.test_manager.project_a + @property + def is_jira_cloud_ci(self) -> bool: + """is running on Jira Cloud""" + return self.test_manager._cloud_ci + def rndstr(): return "".join(random.sample(string.ascii_lowercase, 6)) From 41670cc173e9e3874197064fe4d3151b4ac8af41 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Thu, 29 Jul 2021 21:06:53 +0100 Subject: [PATCH 06/15] create_project leadAccountId inline --- jira/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index d9aec95e5..12ca7ee02 100644 --- a/jira/client.py +++ b/jira/client.py @@ -4049,8 +4049,7 @@ def create_project( "key": key, "projectTypeKey": ptype, "projectTemplateKey": template_key, - "lead": assignee, - # "leadAccountId": assignee, + "leadAccountId" if self._is_cloud else "lead": assignee, "assigneeType": "PROJECT_LEAD", "description": "", # "avatarId": 13946, From 28a5149352a967c4a1b1f90118c3c591f9b4fc5b Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Thu, 29 Jul 2021 17:52:01 +0100 Subject: [PATCH 07/15] fix bugs in setting defaults for create_project --- jira/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jira/client.py b/jira/client.py index 12ca7ee02..3affcfc1f 100644 --- a/jira/client.py +++ b/jira/client.py @@ -3952,31 +3952,31 @@ def create_project( ps_list: List[Dict[str, Any]] - if not permissionScheme: + if permissionScheme is None: ps_list = self.permissionschemes() for sec in ps_list: if sec["name"] == "Default Permission Scheme": permissionScheme = sec["id"] - break - if not permissionScheme: + break + if permissionScheme is None and ps_list: permissionScheme = ps_list[0]["id"] - if not issueSecurityScheme: + if issueSecurityScheme is None: ps_list = self.issuesecurityschemes() for sec in ps_list: if sec["name"] == "Default": # no idea which one is default issueSecurityScheme = sec["id"] - break - if not issueSecurityScheme and ps_list: + break + if issueSecurityScheme is None and ps_list: issueSecurityScheme = ps_list[0]["id"] - if not projectCategory: + if projectCategory is None: ps_list = self.projectcategories() for sec in ps_list: if sec["name"] == "Default": # no idea which one is default projectCategory = sec["id"] - break - if not projectCategory and ps_list: + break + if projectCategory is None and ps_list: projectCategory = ps_list[0]["id"] # Atlassian for failing to provide an API to get projectTemplateKey values # Possible values are just hardcoded and obviously depending on Jira version. From 42d362c298536a20d77888789a9348fa4b8c9494 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Thu, 29 Jul 2021 17:52:21 +0100 Subject: [PATCH 08/15] set default project template for Cloud correctly --- jira/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 3affcfc1f..21816ed14 100644 --- a/jira/client.py +++ b/jira/client.py @@ -3986,7 +3986,9 @@ def create_project( if not template_name: # https://confluence.atlassian.com/jirakb/creating-projects-via-rest-api-in-jira-963651978.html template_key = ( - "com.pyxis.greenhopper.jira:basic-software-development-template" + "com.pyxis.greenhopper.jira:gh-simplified-basic" + if self._is_cloud + else "com.pyxis.greenhopper.jira:basic-software-development-template" ) # https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-projects/#api-rest-api-2-project-get From e4057875463da24ad6ab8dae5f722d0d4850d97c Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Wed, 28 Jul 2021 00:04:20 +0100 Subject: [PATCH 09/15] test_group allow_on_cloud --- tests/resources/test_group.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/resources/test_group.py b/tests/resources/test_group.py index 8be77dd2a..32f6745f5 100644 --- a/tests/resources/test_group.py +++ b/tests/resources/test_group.py @@ -1,15 +1,22 @@ -from tests.conftest import JiraTestCase +from tests.conftest import JiraTestCase, allow_on_cloud +@allow_on_cloud class GroupsTest(JiraTestCase): + def setUp(self) -> None: + JiraTestCase.setUp(self) + self.group_name = ( + "administrators" if self.is_jira_cloud_ci else "jira-administrators" + ) + def test_group(self): - group = self.jira.group("jira-administrators") - self.assertEqual(group.name, "jira-administrators") + group = self.jira.group(self.group_name) + self.assertEqual(group.name, self.group_name) def test_groups(self): groups = self.jira.groups() self.assertGreater(len(groups), 0) def test_groups_for_users(self): - groups = self.jira.groups("jira-administrators") + groups = self.jira.groups(self.group_name) self.assertGreater(len(groups), 0) From e18a816323bc2321b29aacbd05d0028e9600526b Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Fri, 17 Sep 2021 22:54:34 +0100 Subject: [PATCH 10/15] use super() --- tests/conftest.py | 2 +- tests/resources/test_group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 142219ccc..87ef1e55c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,7 @@ class JiraTestCase(unittest.TestCase): def setUp(self) -> None: """ This is called before each test. If you want to add more for your tests, - Run `JiraTestCase.setUp(self) in your custom setUp() to obtain these. + Run `super().setUp() in your custom setUp() to obtain these. """ initialized = False diff --git a/tests/resources/test_group.py b/tests/resources/test_group.py index 32f6745f5..2ad4185a2 100644 --- a/tests/resources/test_group.py +++ b/tests/resources/test_group.py @@ -4,7 +4,7 @@ @allow_on_cloud class GroupsTest(JiraTestCase): def setUp(self) -> None: - JiraTestCase.setUp(self) + super().setUp() self.group_name = ( "administrators" if self.is_jira_cloud_ci else "jira-administrators" ) From 0927b082ded47bd54e47c0ecae987c0ceac60e4e Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Fri, 17 Sep 2021 22:37:44 +0100 Subject: [PATCH 11/15] allow_exist -> force_recreate for clarity --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 87ef1e55c..fadd11146 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -239,11 +239,11 @@ def _remove_project(self, project_key): ) def _create_project( - self, project_key: str, project_name: str, allow_exist: bool = False + self, project_key: str, project_name: str, force_recreate: bool = False ) -> int: """Create a project and return the id""" - if allow_exist and self._project_exists(project_key): + if not force_recreate and self._project_exists(project_key): pass else: self._remove_project(project_key) @@ -293,7 +293,7 @@ def create_some_data(self): self.project_a_id = self._create_project(self.project_a, self.project_a_name) self.project_b_id = self._create_project( - self.project_b, self.project_b_name, allow_exist=True + self.project_b, self.project_b_name, force_recreate=True ) sleep(1) # keep it here as often Jira will report the From 801802bbc11fca8c27ddf5cf543ec220a1fc1d69 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Mon, 4 Oct 2021 19:44:29 +0100 Subject: [PATCH 12/15] update exception handling to use JIRAError over Exception where possible --- tests/conftest.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e3ec7c73e..0aa5732c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ import pytest from jira import JIRA +from jira.exceptions import JIRAError TEST_ROOT = os.path.dirname(__file__) TEST_ICON_PATH = os.path.join(TEST_ROOT, "icon.png") @@ -191,17 +192,15 @@ def set_basic_auth_logins(self, **jira_class_kwargs): **jira_class_kwargs, ) else: - # Setup some un-authenticated users - self.jira_admin = JIRA(self.CI_JIRA_URL, **jira_class_kwargs) - self.jira_sysadmin = JIRA(self.CI_JIRA_URL, **jira_class_kwargs) - self.jira_normal = JIRA(self.CI_JIRA_URL, **jira_class_kwargs) + raise RuntimeError("CI_JIRA_ADMIN environment variable is not set/empty.") def _project_exists(self, project_key: str) -> bool: try: self.jira_admin.project(project_key) - except Exception as e: # If the project does not exist a warning is thrown + except JIRAError as e: # If the project does not exist a warning is thrown if "No project could be found" in str(e): return False + LOGGER.exception("Assuming project '%s' exists.", project_key) return True def _remove_project(self, project_key): @@ -213,17 +212,11 @@ def _remove_project(self, project_key): # TODO(ssbarnea): find a way to prevent SecurityTokenMissing for On Demand # https://jira.atlassian.com/browse/JRA-39153 - try: - self.jira_admin.project(project_key) - except Exception as e: # If the project does not exist a warning is thrown - if "No project could be found" not in str(e): - raise e - else: - # if no error is thrown that means the project exists, so we try to delete it + if self._project_exists(project_key): try: self.jira_admin.delete_project(project_key) - except Exception as e: - LOGGER.warning("Failed to delete %s\n%s", project_key, e) + except Exception: + LOGGER.exception("Failed to delete '%s'.", project_key) # wait for the project to be deleted for _ in range(1, wait_attempts): @@ -252,7 +245,7 @@ def _create_project( try: if self.jira_admin.create_project(project_key, project_name): break - except Exception as e: + except JIRAError as e: if "A project with that name already exists" not in str(e): raise e return self.jira_admin.project(project_key).id From 3f4ccfbf16c4b5b57024611d6facf60db8a46535 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Mon, 4 Oct 2021 19:46:08 +0100 Subject: [PATCH 13/15] add docstring to _project_exists --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 0aa5732c0..90cac71ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -195,6 +195,10 @@ def set_basic_auth_logins(self, **jira_class_kwargs): raise RuntimeError("CI_JIRA_ADMIN environment variable is not set/empty.") def _project_exists(self, project_key: str) -> bool: + """True if we think the project exists, else False. + + Assumes project exists if unknown Jira exception is raised. + """ try: self.jira_admin.project(project_key) except JIRAError as e: # If the project does not exist a warning is thrown From 700deaf7a876baaa00bbc913ac4c7f2d6b2a2607 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 18:46:40 +0000 Subject: [PATCH 14/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 90cac71ff..a8e820a71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,7 +196,7 @@ def set_basic_auth_logins(self, **jira_class_kwargs): def _project_exists(self, project_key: str) -> bool: """True if we think the project exists, else False. - + Assumes project exists if unknown Jira exception is raised. """ try: @@ -216,7 +216,7 @@ def _remove_project(self, project_key): # TODO(ssbarnea): find a way to prevent SecurityTokenMissing for On Demand # https://jira.atlassian.com/browse/JRA-39153 - if self._project_exists(project_key): + if self._project_exists(project_key): try: self.jira_admin.delete_project(project_key) except Exception: From 3b969ff539b734eb8053803cc3d20087e46ed3e2 Mon Sep 17 00:00:00 2001 From: adehad <26027314+adehad@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:10:58 +0100 Subject: [PATCH 15/15] reusable workflows to enforce Server passing before Cloud --- .github/workflows/jira_ci.yml | 24 ++++++++++++++++++++++++ .github/workflows/jira_cloud_ci.yml | 24 +++++++++++++++--------- .github/workflows/jira_server_ci.yml | 10 ++-------- 3 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/jira_ci.yml diff --git a/.github/workflows/jira_ci.yml b/.github/workflows/jira_ci.yml new file mode 100644 index 000000000..7acfe610a --- /dev/null +++ b/.github/workflows/jira_ci.yml @@ -0,0 +1,24 @@ +name: Jira CI + +on: + # Trigger the workflow on push or pull request, + # but only for the main branch + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + Server: + uses: pycontribs/jira/.github/workflows/jira_server_ci.yml@main + + Cloud: + needs: Server + uses: pycontribs/jira/.github/workflows/jira_cloud_ci.yml@main + secrets: + CLOUD_ADMIN: ${{ secrets.CI_JIRA_CLOUD_ADMIN }} + CLOUD_ADMIN_TOKEN: ${{ secrets.CI_JIRA_CLOUD_ADMIN_TOKEN }} + CLOUD_USER: ${{ secrets.CI_JIRA_CLOUD_USER }} + CLOUD_USER_TOKEN: ${{ secrets.CI_JIRA_CLOUD_USER_TOKEN }} diff --git a/.github/workflows/jira_cloud_ci.yml b/.github/workflows/jira_cloud_ci.yml index 0625e373b..8ca913005 100644 --- a/.github/workflows/jira_cloud_ci.yml +++ b/.github/workflows/jira_cloud_ci.yml @@ -1,14 +1,20 @@ name: Jira Cloud CI on: - workflow_run: - workflows: ["Jira Server CI"] - types: - - completed + workflow_call: + secrets: + CLOUD_ADMIN: + required: true + CLOUD_ADMIN_TOKEN: + required: true + CLOUD_USER: + required: true + CLOUD_USER_TOKEN: + required: true + workflow_dispatch: jobs: test: - if: ${{ github.event.workflow_run.conclusion == 'success' }} name: ${{ matrix.os }} / Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest strategy: @@ -52,10 +58,10 @@ jobs: run: tox -e py38 -- -m allow_on_cloud env: CI_JIRA_TYPE: CLOUD - CI_JIRA_CLOUD_ADMIN: ${{ secrets.CI_JIRA_CLOUD_ADMIN }} - CI_JIRA_CLOUD_ADMIN_TOKEN: ${{ secrets.CI_JIRA_CLOUD_ADMIN_TOKEN }} - CI_JIRA_CLOUD_USER: ${{ secrets.CI_JIRA_CLOUD_USER }} - CI_JIRA_CLOUD_USER_TOKEN: ${{ secrets.CI_JIRA_CLOUD_USER_TOKEN }} + CI_JIRA_CLOUD_ADMIN: ${{ secrets.CLOUD_ADMIN }} + CI_JIRA_CLOUD_ADMIN_TOKEN: ${{ secrets.CLOUD_ADMIN_TOKEN }} + CI_JIRA_CLOUD_USER: ${{ secrets.CLOUD_USER }} + CI_JIRA_CLOUD_USER_TOKEN: ${{ secrets.CLOUD_USER_TOKEN }} - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.0.15 diff --git a/.github/workflows/jira_server_ci.yml b/.github/workflows/jira_server_ci.yml index 515c803f3..dd130ea33 100644 --- a/.github/workflows/jira_server_ci.yml +++ b/.github/workflows/jira_server_ci.yml @@ -1,14 +1,8 @@ name: Jira Server CI on: - # Trigger the workflow on push or pull request, - # but only for the main branch - push: - branches: - - main - pull_request: - branches: - - main + workflow_call: + workflow_dispatch: jobs: test: