#157 [WIP] pdc_unretire_packages plugin
Merged 11 months ago by humaton. Opened a year ago by amedvede.
fedora-infra/ amedvede/toddlers feature_plugin  into  main

The added file is too large to be shown here, see it at: tests/plugins/test_pdc_unretire_packages.py
file modified
+132
@@ -94,3 +94,135 @@ 

          )

  

          assert commit is None

+ 

+ 

+ class TestGitRepoGetLastCommitMessage:

+     """

+     Test class for `toddlers.utils.git.GitRepo.get_last_commit_message` method.

+     """

+ 

+     def setup(self):

+         """

+         Initialize the GitRepo object.

+         """

+         repo_mock = Mock()

+ 

+         self.repo = GitRepo(repo_mock)

+ 

+         assert self.repo.repo == repo_mock

+ 

+     def test_get_last_commit_message(self):

+         """

+         Assert that getting last commit message works correctly.

+         """

+         result = "this is commit message"

+ 

+         commit_mock = Mock()

+         commit_mock.message = "this is commit message\n"

+ 

+         self.repo.repo.commit.return_value = commit_mock

+ 

+         last_commit_message = self.repo.get_last_commit_message()

+ 

+         self.repo.repo.commit.assert_called_once()

+ 

+         assert last_commit_message == result

+ 

+     def test_get_last_commit_message_no_commit(self):

+         """

+         Assert that getting last commit message works correctly, when there is no commit.

+         """

+         commit = ""

+ 

+         self.repo.repo.commit.return_value = commit

+ 

+         last_commit_message = self.repo.get_last_commit_message()

+ 

+         self.repo.repo.commit.assert_called_once()

+ 

+         assert last_commit_message is None

+ 

+ 

+ class TestGitRepoGetLastCommitDate:

+     """

+     Test class for `toddlers.utils.git.GitRepo.get_last_commit_date` method.

+     """

+ 

+     def setup(self):

+         """

+         Initialize the GitRepo object.

+         """

+         repo_mock = Mock()

+ 

+         self.repo = GitRepo(repo_mock)

+ 

+         assert self.repo.repo == repo_mock

+ 

+     def test_get_last_commit_date(self):

+         """

+         Assert that getting last commit date works correctly.

+         """

+         result = 1234

+ 

+         commit_mock = Mock()

+         commit_mock.committed_date = 1234

+ 

+         self.repo.repo.commit.return_value = commit_mock

+ 

+         last_commit_date = self.repo.get_last_commit_date()

+ 

+         self.repo.repo.commit.assert_called_once()

+ 

+         assert last_commit_date == result

+ 

+     def test_get_last_commit_date_no_commit(self):

+         """

+         Assert that getting last commit date works correctly, when there is no commit.

+         """

+         commit = ""

+ 

+         self.repo.repo.commit.return_value = commit

+ 

+         last_commit_date = self.repo.get_last_commit_date()

+ 

+         self.repo.repo.commit.assert_called_once()

+ 

+         assert last_commit_date is None

+ 

+ 

+ class TestGitRepoRevertLastCommit:

+     """

+     Test class for `toddlers.utils.git.GitRepo.revert_last_commit` method.

+     """

+ 

+     def setup(self):

+         """

+         Initialize the GitRepo object.

+         """

+         repo_mock = Mock()

+ 

+         self.repo = GitRepo(repo_mock)

+ 

+         assert self.repo.repo == repo_mock

+ 

+     def test_revert_last_commit(self):

+         """

+         Assert that revert last commit process correctly.

+         """

+         message = "revert commit message"

+         commit_hash = 1234

+ 

+         commit_mock = Mock()

+         commit_mock.hexsha = commit_hash

+         origin_mock = Mock()

+ 

+         self.repo.repo.commit.return_value = commit_mock

+         self.repo.repo.remote.return_value = origin_mock

+ 

+         self.repo.revert_last_commit(message)

+ 

+         self.repo.repo.git.execute.assert_called_with(

+             ["git", "checkout", f"{commit_hash}", "."]

+         )

+         self.repo.repo.index.commit.assert_called_with(message=message)

+         origin_mock.push.assert_called_once()

file modified
+162 -3
@@ -34,7 +34,7 @@ 

          Test initialization of pagure module without required config value.

          """

          with pytest.raises(

-             ValueError, match=r"No pagure_url found in the configuration file"

+                 ValueError, match=r"No pagure_url found in the configuration file"

          ):

              pagure.set_pagure({})

  
@@ -43,7 +43,7 @@ 

          Test initialization of pagure module without required config value.

          """

          with pytest.raises(

-             ValueError, match=r"No pagure_api_key found in the configuration file"

+                 ValueError, match=r"No pagure_api_key found in the configuration file"

          ):

              config = {"pagure_url": "https://pagure.io"}

              pagure.set_pagure(config)
@@ -201,7 +201,7 @@ 

  

          with pytest.raises(PagureError, match=expected_error):

              with patch(

-                 "toddlers.utils.pagure.Pagure.add_comment_to_issue"

+                     "toddlers.utils.pagure.Pagure.add_comment_to_issue"

              ) as comment_mock:

                  comment_mock.return_value = True

                  self.pagure.close_issue(issue_id, namespace, message, reason)
@@ -1289,3 +1289,162 @@ 

              "https://pagure.io/api/0/{0}/{1}".format(namespace, repo),

              headers=self.pagure.get_auth_header(),

          )

+ 

+ 

+ class TestPagureIsProjectOrphaned:

+     """

+     Test class for `toddlers.pagure.Pagure.is_project_orphaned` method.

+     """

+ 

+     def setup(self):

+         """

+         Setup method for the test class.

+         """

+         config = {

+             "pagure_url": "https://pagure.io",

+             "pagure_api_key": "Very secret key",

+         }

+         self.pagure = pagure.set_pagure(config)

+         self.pagure._requests_session = Mock()

+ 

+     def test_is_project_orphaned_project_orphaned(self):

+         """

+         Assert that checking if project is orphaned works correctly. Project is orphaned.

+         """

+         response_mock = Mock()

+         response_mock.ok = True

+ 

+         keyword = "orphan"

+         data = {"epel_assignee": keyword,

+                 "fedora_assignee": "some_fas"}

+ 

+         response_mock.json.return_value = data

+ 

+         self.pagure._requests_session.get.return_value = response_mock

+ 

+         namespace = "namespace"

+         repo = "repo"

+ 

+         result = self.pagure.is_project_orphaned(namespace, repo)

+ 

+         self.pagure._requests_session.get.assert_called_with(

+             "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo,

+             headers=self.pagure.get_auth_header(),

+         )

+ 

+         assert result is True

+ 

+     def test_is_project_orphaned_project_not_orphaned(self):

+         """

+         Assert that checking if project is orphaned works correctly. Project is not orphaned.

+         """

+         response_mock = Mock()

+         response_mock.ok = True

+ 

+         data = {"epel_assignee": "some_fas",

+                 "fedora_assignee": "some_fas"}

+ 

+         response_mock.json.return_value = data

+ 

+         self.pagure._requests_session.get.return_value = response_mock

+ 

+         namespace = "namespace"

+         repo = "repo"

+ 

+         result = self.pagure.is_project_orphaned(namespace, repo)

+ 

+         self.pagure._requests_session.get.assert_called_with(

+             "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo,

+             headers=self.pagure.get_auth_header(),

+         )

+ 

+         assert result is False

+ 

+     def test_is_project_orphaned_failure(self):

+         """

+         Assert that checking if project is orphaned will raise error if the project doesn't exist.

+         """

+         response_mock = Mock()

+         response_mock.ok = False

+         response_mock.status_code = 404

+ 

+         self.pagure._requests_session.get.return_value = response_mock

+ 

+         namespace = "namespace"

+         repo = "repo"

+ 

+         expected_error = "Couldn't get project '{0}/{1}' maintainers.".format(namespace, repo)

+ 

+         with pytest.raises(PagureError, match=expected_error):

+             self.pagure.is_project_orphaned(namespace, repo)

+ 

+         self.pagure._requests_session.get.assert_called_with(

+             "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo,

+             headers=self.pagure.get_auth_header(),

+         )

+ 

+ 

+ class TestPagureAssignMaintainerToProject:

+     """

+     Test class for `toddlers.pagure.Pagure.assign_maintainer_to_project` method.

+     """

+ 

+     def setup(self):

+         """

+         Setup method for the test class.

+         """

+         config = {

+             "pagure_url": "https://pagure.io",

+             "pagure_api_key": "Very secret key",

+         }

+         self.pagure = pagure.set_pagure(config)

+         self.pagure._requests_session = Mock()

+ 

+     def test_assign_maintainer_to_project(self):

+         """

+         Assert that assigning maintainer to project is processed correctly.

+         """

+         response_mock = Mock()

+         response_mock.status_code = 200

+ 

+         self.pagure._requests_session.patch.return_value = response_mock

+ 

+         namespace = "namespace"

+         repo = "repo"

+         maintainer_fas = "amedvede"

+         payload = {"EPEL Maintainer name": maintainer_fas,

+                    "Maintainer name": maintainer_fas}

+ 

+         self.pagure.assign_maintainer_to_project(namespace, repo, maintainer_fas)

+ 

+         self.pagure._requests_session.patch.assert_called_with(

+             "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo,

+             data=json.dumps(payload),

+             headers=self.pagure.get_auth_header(),

+         )

+ 

+     def test_assign_maintainer_to_project_failure(self):

+         """

+         Assert that failing to assign maintainer is handled correctly.

+         """

+         response_mock = Mock()

+         response_mock.status_code = 500

+ 

+         self.pagure._requests_session.patch.side_effect = response_mock

+ 

+         namespace = "namespace"

+         repo = "repo"

+         maintainer_fas = "amedvede"

+         payload = {"EPEL Maintainer name": maintainer_fas,

+                    "Maintainer name": maintainer_fas}

+ 

+         expected_error = "Couldn't set new maintainer on project '{0}/{1}'".format(namespace, repo)

+ 

+         with pytest.raises(PagureError, match=expected_error):

+             self.pagure.assign_maintainer_to_project(namespace, repo, maintainer_fas)

+ 

+         self.pagure._requests_session.patch.assert_called_with(

+             "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo,

+             data=json.dumps(payload),

+             headers=self.pagure.get_auth_header(),

+         )

file modified
+109
@@ -447,3 +447,112 @@ 

                  call({"name": global_component, "dist_git_web_url": dist_git_url}),

              ]

          )

+ 

+ 

+ class TestPdcGetBranchSlas:

+     """

+     Test class for `toddlers.utils.pdc.get_branch_slas` function.

+     """

+ 

+     def setup(self):

+         """

+         Setup the PDC module.

+         """

+         pdc._PDC = MagicMock()

+ 

+     def test_get_branch_slas(self):

+         """

+         Assert that correct response is handled.

+         """

+         response = {"count": 1, "results": [{"id": 3}]}

+ 

+         global_component = "global_component"

+         component_type = "rpm"

+         branch = "f38"

+ 

+         pdc._PDC["component-branch-slas"]._.return_value = response

+ 

+         result = pdc.get_branch_slas(global_component, component_type, branch)

+ 

+         assert result == [{"id": 3}]

+ 

+         pdc._PDC["component-branch-slas"]._.assert_called_with(

+             global_component=global_component,

+             branch_type=component_type,

+             branch=branch,

+         )

+ 

+     def test_get_global_component_not_found(self):

+         """

+         Assert that incorrect response is handled.

+         """

+         response = {"count": 0, "results": []}

+ 

+         global_component = "global_component"

+         component_type = "rpm"

+         branch = "f38"

+ 

+         pdc._PDC["component-branch-slas"]._.return_value = response

+ 

+         result = pdc.get_branch_slas(global_component, component_type, branch)

+ 

+         assert not result

+ 

+         pdc._PDC["component-branch-slas"]._.assert_called_with(

+             global_component=global_component,

+             branch_type=component_type,

+             branch=branch,

+         )

+ 

+ 

+ class TestPdcAdjustEol:

+     """

+     Test class for `toddlers.utils.pdc.adjust_eol` function.

+     """

+ 

+     def setup(self):

+         """

+         Setup the PDC module.

+         """

+         pdc._PDC = MagicMock()

+         pdc.get_branch_slas = MagicMock()

+ 

+     def test_adjust_eol_branch_doesnt_have_slas(self):

+         """

+         Assert that func won't do anything if branch doesn't have sla.

+         """

+         response = None

+ 

+         global_component = "global_component"

+         component_type = "rpm"

+         branch = "f38"

+         eol = "2023-10-16"

+ 

+         pdc.get_branch_slas.return_value = response

+ 

+         pdc.adjust_eol(global_component, component_type, branch, eol)

+ 

+         pdc._PDC["component-branch-slas"][3]._.assert_not_called()

+ 

+     def test_adjust_eol(self):

+         """

+         Assert that all slas of branch will be updated.

+         """

+         response = [{"id": 3}, {"id": 4}, {"id": 5}]

+ 

+         global_component = "global_component"

+         component_type = "rpm"

+         branch = "f38"

+         eol = "2023-10-16"

+ 

+         pdc.get_branch_slas.return_value = response

+ 

+         pdc.adjust_eol(global_component, component_type, branch, eol)

+ 

+         for sla in response:

+             pdc._PDC["component-branch-slas"][sla["id"]]._.assert_called_with(

+                 {

+                     "eol": eol,

+                     "branch_active": True,

+                 }

+             )

@@ -0,0 +1,780 @@ 

+ """

+ This is a script to automate unretirement of package automatically, when ticket is created.

+ 

+ Authors:    Anton Medvedev <amedvede@redhat.com>

+ 

+ """

+ import argparse

+ import json

+ import logging

+ import sys

+ import re

+ import datetime

+ import tempfile

+ from typing import Optional

+ from git import GitCommandError

+ 

+ import toml

+ import requests

+ import koji

+ 

+ from fedora_messaging.api import Message

+ from pagure_messages.issue_schema import IssueNewV1

+ 

+ from toddlers.base import ToddlerBase

+ from toddlers.exceptions import ValidationError

+ from toddlers.utils import pagure, git, bugzilla_system, pdc

+ 

+ # Regex for bugzilla url

+ BUGZILLA_URL_REGEX = r"https:\/\/bugzilla\.redhat\.com(.+)(id=\d{7})"

+ # Regex for branches

+ BRANCHES_REGEX = r"f\d{2}|rawhide"

+ # Where to look for pdc_unretire tickets

+ PROJECT_NAMESPACE = "releng"

+ # Keyword that will be searched for in the issue title

+ UNRETIRE_KEYWORD = "unretire"

+ # Forbidden keywords for commit message

+ FORBIDDEN_KEYWORDS_FOR_COMMIT_MESSAGE = ["legal", "license"]

+ # Time difference limit not getting Bugzilla url

+ TIME_DIFFERENCE_LIMIT = 56  # 8 weeks in days

+ # Package retirement process url

+ PACKAGE_RETIREMENT_PROCESS_URL = (

+     "https://docs.fedoraproject.org/en-US/package-maintainers"

+     "/Package_Retirement_Process/#claiming"

+ )

+ # Fedora review bugzilla tag

+ FEDORA_REVIEW_TAG_NAME = "fedora-review"

+ # Koji hub url

+ KOJIHUB_URL = "https://koji.fedoraproject.org/kojihub"

+ # Set certain parts not ot run if it's not in production

+ DEVEL = False

+ 

+ _log = logging.getLogger(__name__)

+ 

+ 

+ class PDCUnretirePackages(ToddlerBase):

+     """

+     Listen for new tickets in https://pagure.io/releng/issues

+     and process then, either by unretiring a package or rejecting the ticket

+     """

+ 

+     name: str = "pdc_unretire_packages"

+ 

+     amqp_topics: dict = ["io.pagure.prod.pagure.issue.new"]

+ 

+     # Dist-git base url

+     dist_git_base: str = ""

+ 

+     # TODO probably not working with mypy

+     # PDC client object connected to pdc

+     pdc_client: pdc.PDCClient

+ 

+     # Pagure object connected to pagure.io

+     pagure_io: pagure.Pagure

+ 

+     # Pagure object conected to dist-git

+     # dist_git: pagure.Pagure

+ 

+     # Git repo object

+     git_repo: git.GitRepo

+ 

+     # Koji session object

+     koji_session: koji.ClientSession

+ 

+     def accepts_topic(self, topic: str) -> bool:

+         """

+         Returns a boolean whether this toddler is interested in messages

+         from this specific topic.

+ 

+         :arg topic: Topic to check

+ 

+         :returns: True if topic is accepted, False otherwise

+         """

+         return topic == "io.pagure.prod.pagure.issue.new"

+ 

+     def process(

+             self,

+             config: dict,

+             message: Message,

+     ) -> None:

+         """

+         Process a given message.

+ 

+         :arg config: Toddlers configuration

+         :arg message: Message to process

+         """

+         _log.debug(

+             "Processing message:\n{0}".format(json.dumps(message.body, indent=2))

+         )

+         project_name = message.body["project"]["fullname"]

+ 

+         if project_name != PROJECT_NAMESPACE:

+             _log.info(

+                 "The message doesn't belong to project {0}. Skipping message.".format(

+                     PROJECT_NAMESPACE

+                 )

+             )

+             return

+ 

+         issue = message.body["issue"]

+ 

+         if issue["status"] != "Open":

+             _log.info(

+                 "The issue {0} is not open. Skipping message.".format(issue["id"])

+             )

+             return

+ 

+         self.dist_git_base = config.get("dist_git_url")

+ 

+         issue_title = issue["title"]

+ 

+         if UNRETIRE_KEYWORD not in issue_title.lower():

+             _log.info(

+                 "The issue doesn't contain keyword '{0}' in the title '{1}'".format(

+                     UNRETIRE_KEYWORD, issue_title

+                 )

+             )

+             return

+ 

+         _log.info("Setting up connection to PDC")

+         self.pdc_client = pdc.pdc_client_for_config(config)

+ 

+         _log.info("Setting up connection to Pagure")

+         self.pagure_io = pagure.set_pagure(config)

+ 

+         # _log.info("Setting up connection to Dist-Git")

+         # dist_git_config = {

+         #     "pagure_url": config.get("dist_git_url"),

+         #     "pagure_api_key": config.get("dist_git_token")

+         # }

+         # self.dist_git = pagure.set_pagure(dist_git_config)

+ 

+         _log.info("Setting up connection to Bugzilla")

+         bugzilla_system.set_bz(config)

+ 

+         _log.info("Setting up session with Koji")

+         self.koji_session = koji.ClientSession(KOJIHUB_URL)

+ 

+         self.process_ticket(issue)

+ 

+     def process_ticket(self, issue: dict) -> None:

+         """

+         Process a single ticket

+ 

+         :arg issue: A dictionary containing the issue

+         """

+         _log.info("Handling pagure releng ticket '{0}'".format(issue["full_url"]))

+ 

+         issue_title = issue["title"]

+         issue_body = issue["content"].strip("`").strip("\n")

+         issue_url = issue["full_url"]

+         issue_opener = issue["user"]["name"]

+         issue_id = issue["id"]

+ 

+         # improve searching for package name

+         end_of_keyword_in_title_index = (

+                 issue_title.lower().index(UNRETIRE_KEYWORD) + len(UNRETIRE_KEYWORD) + 1

+         )

+ 

+         inaccurate_package_name = issue_title[end_of_keyword_in_title_index:]

+         list_of_words_in_package_name = [

+             word.strip() for word in inaccurate_package_name.split(" ")

+         ]

+ 

+         if len(list_of_words_in_package_name) > 1:

+             _log.info(

+                 "Package name '{0}' has two or more words".format(

+                     inaccurate_package_name

+                 )

+             )

+             self.pagure_io.close_issue(

+                 issue_id,

+                 namespace=PROJECT_NAMESPACE,

+                 message="Invalid package name",

+                 reason="Invalid",

+             )

+             return

+ 

+         inaccurate_package_name = list_of_words_in_package_name[0]

+         cloned_repo_name = "{0}_repo".format(inaccurate_package_name.replace("/", "_"))

+         package_url = self._get_package_url(inaccurate_package_name)

+ 

+         if not package_url:

+             _log.info(

+                 "Package with this name '{0}' wasn't found on dist-git".format(

+                     inaccurate_package_name

+                 )

+             )

+             self.pagure_io.close_issue(

+                 issue_id,

+                 namespace=PROJECT_NAMESPACE,

+                 message="Package wasn't found on dist-git",

+                 reason="Invalid",

+             )

+             return

+ 

+         namespace, repo = self._get_namespace_and_repo(package_url)

+ 

+         _log.info("Creating temporary directory")

+         with tempfile.TemporaryDirectory():

+             _log.info("Cloning repo into dir with name '{0}'".format(cloned_repo_name))

+             try:

+                 self.git_repo = git.clone_repo(package_url, cloned_repo_name)

+             except GitCommandError:

+                 message = "Remote repository doesn't exist or you do not have the appropriate " \

+                           "permissions to access it."

+                 _log.info(message)

+                 self.pagure_io.close_issue(

+                     issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Invalid",

+                 )

+                 return

+ 

+             tags_to_unblock = self._verify_package_ready_for_unretirement(

+                 issue_id=issue_id, issue_body=issue_body

+             )

+ 

+             _log.info("Reverting retire commit")

+             revert_commit_message = "Unretirement request: {0}".format(issue_url)

+             self.git_repo.revert_last_commit(message=revert_commit_message)

+ 

+             _log.info("Unblocking tags on Koji.")

+             if self._is_need_to_unblock_tags_on_koji(tags_to_unblock, repo):

+                 self._unblock_tags_on_koji(issue_id, tags_to_unblock, repo)

+ 

+             _log.info("Verifying package is not orphan.")

+             if self.pagure_io.is_project_orphaned(namespace=namespace, repo=repo):

+                 self.pagure_io.assign_maintainer_to_project(

+                     namespace=namespace, repo=repo, maintainer_fas=issue_opener

+                 )

+ 

+             _log.info("Adjusting eol on PDC.")

+             self._adjust_eol_pdc(namespace, repo, tags_to_unblock)

+ 

+         _log.info("everything was alright!")

+         return

+ 

+     def _get_package_url(self, package_name: str) -> Optional[str]:

+         """

+         Return a package url.

+ 

+         :arg package_name: A package name with or without namespace

+ 

+         :returns: Package dist-git url or 'not_found' if url doesn't exist.

+         """

+         package_namespaces = ("rpms/", "modules/", "container/")

+ 

+         if package_name.startswith(package_namespaces):

+             url = f"{self.dist_git_base}/{package_name}.git"

+             if self._is_url_exist(url):

+                 return url

+         else:

+             for namespace in package_namespaces:

+                 url = f"{self.dist_git_base}/{namespace}/{package_name}.git"

+                 if self._is_url_exist(url):

+                     return url

+         return

+ 

+     @staticmethod

+     def _get_namespace_and_repo(package_url: str) -> Optional[tuple]:

+         """

+         Get namespace and package name from package url of exist package.

+ 

+         :arg package_url: A package url

+ 

+         :returns: A tuple with package namespace and package name

+         """

+         suffix = ".git"

+         if package_url.endswith(suffix):

+             url_without_suffix = package_url[: len(package_url) - len(suffix)]

+         else:

+             url_without_suffix = package_url

+ 

+         list_of_elements = url_without_suffix.split("/")

+         list_of_elements = [el for el in list_of_elements if el != ""]

+ 

+         if len(list_of_elements) == 4:

+             package_name = list_of_elements[-1]

+             namespace = list_of_elements[-2]

+             return namespace, package_name

+         else:

+             _log.info("Something wrong with url '{0}'".format(package_url))

+             return None, None

+ 

+     def _verify_package_ready_for_unretirement(

+             self, issue_id: int, issue_body: str

+     ) -> list:

+         """

+         Verify that package is ready for unretirement.

+ 

+         :arg issue_id: AN int value of issue ID.

+         :arg issue_body: A str contain body of issue.

+ 

+         :returns: A list with tags requester ask to unblock.

+         """

+         _log.info("Verifying that package is ready for unretirement")

+ 

+         if not self._is_package_not_retired_for_reason(issue_id=issue_id):

+             raise ValidationError(

+                 "Package was retired for a reason: legal of license issue."

+             )

+ 

+         if not self._is_last_commit_date_correct(issue_id=issue_id):

+             raise ValidationError("Last commit was made more than 8 weeks ago.")

+ 

+         if not self._verify_bugzilla_ticket(issue_id, issue_body):

+             raise ValidationError(

+                 "Package on bugzilla was not verified to be unretired."

+             )

+ 

+         tags_to_unblock = self._get_tags_to_unblock(issue_body)

+         return tags_to_unblock

+ 

+     def _is_package_not_retired_for_reason(self, issue_id: int) -> bool:

+         """

+         Verify that commit message does not contain forbidden keywords

+ 

+         :arg issue_id: An int value of issue ID.

+ 

+         :returns: Bool value whether package have legal issues

+         """

+         last_commit_message = self.git_repo.get_last_commit_message()

+ 

+         _log.info("Verifying that issue message doesn't contain forbidden keywords")

+         if any(

+                 re.search(forbidden_keyword, last_commit_message.lower())

+                 for forbidden_keyword in FORBIDDEN_KEYWORDS_FOR_COMMIT_MESSAGE

+         ):

+             _log.info(

+                 "Commit message '{0}' contain forbidden keyword".format(

+                     last_commit_message

+                 )

+             )

+ 

+             message = "Package was retire because of legal or license issue. Commit message '{0}'" \

+                       "".format(last_commit_message)

+ 

+             if not DEVEL:

+                 self.pagure_io.close_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Closed",

+                 )

+             return False

+         else:

+             return True

+ 

+     def _is_last_commit_date_correct(self, issue_id: int) -> bool:

+         """

+         Verify that last commit was made less than 8 weeks ago.

+ 

+         :arg issue_id: An int value of issue ID.

+ 

+         :returns: A bool value whether last commit was made more than 8 weeks ago

+         """

+         last_commit_date = self.git_repo.get_last_commit_date()

+         last_commit_date_formatted = datetime.datetime.fromtimestamp(last_commit_date)

+         current_time = datetime.datetime.now()

+ 

+         time_diff = current_time - last_commit_date_formatted

+         time_diff_in_days = time_diff.days

+ 

+         if time_diff_in_days <= TIME_DIFFERENCE_LIMIT:

+             return True

+         else:

+             _log.info(

+                 "Last commit date is more then 8 weeks (56 days) it's actually {0}".format(

+                     time_diff_in_days

+                 )

+             )

+ 

+             message = "Package can not be unretired, because retire commit was made more than 8 " \

+                       "weeks ago."

+ 

+             if not DEVEL:

+                 self.pagure_io.close_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Closed",

+                 )

+             return False

+ 

+     def _verify_bugzilla_ticket(self, issue_id: int, issue_body: str) -> bool:

+         """

+         Check whether package exist on bugzilla and whether it has a tags

+ 

+         :arg issue_id: Int value of issue id

+         :arg issue_body: Str with body of issue

+ 

+         :returns: A bool value whether check succeed

+         """

+         _log.info("Getting bugzilla url from issue body")

+ 

+         match = re.search(BUGZILLA_URL_REGEX, issue_body)

+         if match:

+             bugzilla_url = match.group()

+         else:

+             bugzilla_url = None

+ 

+         if not bugzilla_url:

+             _log.info("Issue body doesn't contain a bugzilla url")

+ 

+             comment = (

+                 "The package is retired for more than 8 Weeks as per {0}\n"

+                 "We require a re-review to unretire this package.\n\n"

+                 "Once you have BZ with fedora_review+ please reopen this ticket."

+                 "".format(PACKAGE_RETIREMENT_PROCESS_URL)

+             )

+ 

+             if not DEVEL:

+                 self.pagure_io.add_comment_to_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     comment=comment,

+                 )

+ 

+             _log.info("Closing issue, because bugzilla url wasn't found")

+ 

+             message = "Bugzilla url is missing, please add it and reopen ticket."

+ 

+             if not DEVEL:

+                 self.pagure_io.close_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Get back later",

+                 )

+             return False

+ 

+         bug_id = bugzilla_url.split("id=")[1]

+         try:

+             _log.info("Getting bug with this id '{0}' data".format(bug_id))

+ 

+             bug = bugzilla_system.get_bug(bug_id)

+         except Exception as error:

+             message = "Bugzilla can't get the bug by bug id, fix bugzilla url."

+             if not DEVEL:

+                 self.pagure_io.close_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Get back later",

+                 )

+             raise ValidationError(

+                 "The Bugzilla bug could not be verified. The following "

+                 "error was encountered: {0}".format(str(error))

+             )

+ 

+         try:

+             _log.info("Getting {0} tag from bug".format(FEDORA_REVIEW_TAG_NAME))

+ 

+             fedora_review_tag = bug.get_flags(FEDORA_REVIEW_TAG_NAME)

+             fedora_review_tag_status = fedora_review_tag[0]["status"]

+ 

+             if fedora_review_tag_status != "+":

+                 message = (

+                     "Bug tag '{0}' don't have required status '{1}', "

+                     "but have '{2}' instead.\n"

+                     "Closing ticket".format(

+                         FEDORA_REVIEW_TAG_NAME, "+", fedora_review_tag_status

+                     )

+                 )

+                 _log.info(message)

+ 

+                 if not DEVEL:

+                     self.pagure_io.close_issue(

+                         issue_id=issue_id,

+                         namespace=PROJECT_NAMESPACE,

+                         message=message,

+                         reason="Closed",

+                     )

+                 return False

+ 

+         except TypeError:

+             message = "Tag fedora-review is missing on bugzilla, get it and reopen ticket later."

+             if not DEVEL:

+                 self.pagure_io.close_issue(

+                     issue_id=issue_id,

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Get back later",

+                 )

+             raise ValidationError(

+                 "`{0}` tag is missing.".format(FEDORA_REVIEW_TAG_NAME)

+             )

+ 

+         return True

+ 

+     def _get_tags_to_unblock(self, issue_body: str) -> list:

+         """

+         Get tags requester would like to unblock.

+ 

+         :arg issue_body: A str value with text of issue body

+ 

+         :returns: A list with tags requester would like to unblock

+         """

+         tags = re.findall(BRANCHES_REGEX, issue_body)

+         if len(tags) == 0:

+             tags = ["rawhide"]

+ 

+         last_branch_name = self._get_last_fedora_version_branch_name()

+ 

+         tags_to_unblock = list(

+             map(lambda el: el.replace("rawhide", last_branch_name), tags)

+         )

+ 

+         return tags_to_unblock

+ 

+     @staticmethod

+     def _get_last_fedora_version_branch_name() -> str:

+         """

+         Get last fedora version branch name.

+ 

+         :returns: String with name

+         """

+         bodhi_url = "https://bodhi.fedoraproject.org/releases/"

+         response = requests.get(url=bodhi_url)

+         count_of_version_on_bodhi = int(response.json()["total"])

+ 

+         params = {"rows_per_page": count_of_version_on_bodhi}

+         response = requests.get(url=bodhi_url, params=params)

+ 

+         versions = response.json()["releases"]

+         last_version = [

+             version

+             for version in versions

+             if version["state"] == "pending" and version["branch"] == "rawhide"

+         ]

+         if len(last_version) != 1:

+             raise ValidationError("Not able to find a rawhide branch.")

+ 

+         last_version_name = last_version[0]["dist_tag"]

+         return last_version_name

+ 

+     def _is_need_to_unblock_tags_on_koji(

+             self, tags_to_unblock: list, repo: str

+     ) -> bool:

+         """

+         Check if at least any of the tags requested to be unblocked are really blocked.

+ 

+         :arg tags_to_unblock: List of branch names

+         :arg repo: Name of package

+ 

+         :returns: Bool value whether program need to unblock tags

+         """

+         _log.info("Verifying that tags are blocked on koji.")

+         try:

+             package_tags = self.koji_session.listTags(package=repo)

+             if not package_tags:

+                 raise ValidationError("Package doesn't have tags on koji.")

+             tags_that_suppose_to_be_blocked = []

+ 

+             for tag in package_tags:

+                 prefix = "dist-"

+                 if tag["name"].startswith(prefix):

+                     tag_name = tag["name"][len(prefix):]

+                     if tag_name in tags_to_unblock:

+                         tags_that_suppose_to_be_blocked.append(tag)

+ 

+             if len(tags_that_suppose_to_be_blocked) == 0:

+                 raise ValidationError(

+                     "Request to unblock tags that are not exist on koji."

+                 )

+             return any([tag["locked"] for tag in tags_that_suppose_to_be_blocked])

+         except koji.GenericError:

+             raise ValidationError("Package doesn't exist on koji.")

+ 

+     def _unblock_tags_on_koji(

+             self, issue_id: int, tags_to_unblock: list, repo: str

+     ) -> None:

+         """

+         Unblock certain tags for package.

+ 

+         :arg issue_id: Int value of issue id.

+         :arg tags_to_unblock: List of tags to be unblocked

+         :arg package_name: Package name without namespace

+         """

+         _log.info("Unblocking tags in koji.")

+         if not DEVEL:

+             for tag in tags_to_unblock:

+                 try:

+                     self.koji_session.packageListUnblock(taginfo=tag, pkginfo=repo)

+                 except koji.GenericError:

+                     comment = "Not able to unblock `{0}` tag on koji.".format(tag)

+                     self.pagure_io.add_comment_to_issue(

+                         issue_id=issue_id,

+                         namespace=PROJECT_NAMESPACE,

+                         comment=comment,

+                     )

+ 

+     def _adjust_eol_pdc(self, namespace, repo, branches: list):

+         """

+         Adjusting eol of requested branches.

+ 

+         Params:

+             namespace: A string of repo namespace

+             repo: A string of repo name

+             branches: A list of str of branches requester would like to unretire.

+         """

+         global_component = repo

+         component_type = self._namespace_to_pdc(namespace)

+         branches_with_eol = self._get_proper_eol_for_branches(branches)

+ 

+         for key, value in branches_with_eol.items():

+             branch = key

+             eol = value

+             pdc.adjust_eol(global_component=global_component, component_type=component_type,

+                            branch=branch, eol=eol)

+ 

+     @staticmethod

+     def _namespace_to_pdc(namespace):

+         """Internal method to translate a dist-git namespace to a PDC

+         component type."""

+         namespace_to_pdc = {

+             "rpms": "rpm",

+             "modules": "module",

+             "container": "container",

+         }

+         if namespace not in namespace_to_pdc:

+             raise ValueError('The namespace "{0}" is not supported'.format(namespace))

+         else:

+             return namespace_to_pdc[namespace]

+ 

+     def _get_proper_eol_for_branches(self, tags_to_unblock: list) -> dict:

+         """

+         Get proper eol date for each branch and create the dict with it.

+ 

+         :arg tags_to_unblock: Tags that requester would like to unblock.

+ 

+         :returns: Dictionary with key as branch name and value as eol.

+         """

+         last_branch = self._get_last_fedora_version_branch_name()

+ 

+         bodhi_endpoint = "https://bodhi.fedoraproject.org/releases/"

+         response = requests.get(url=bodhi_endpoint)

+         count_of_version_on_bodhi = response.json()["total"]

+ 

+         params = {"rows_per_page": count_of_version_on_bodhi}

+         response = requests.get(url=bodhi_endpoint, params=params)

+ 

+         releases = response.json()["releases"]

+         branches = {}

+ 

+         for release in releases:

+             tag = release["dist_tag"]

+             eol = release["eol"]

+             if tag in tags_to_unblock:

+                 if tag == last_branch:

+                     branches["rawhide"] = "2222-01-01"

+                 else:

+                     branches[tag] = eol

+ 

+         return branches

+ 

+     @staticmethod

+     def _is_url_exist(url: str) -> bool:

+         """

+         Check whether url exist.

+ 

+         :arg url: Url that might exist

+ 

+         :returns: True if url exist, otherwise False

+         """

+         response = requests.get(url=url)

+         return response.status_code == 200

+ 

+ 

+ def _get_arguments(args):

+     """Load and parse the CLI arguments.

+ 

+     :arg args: Script arguments

+ 

+     :returns: Parsed arguments

+     """

+     parser = argparse.ArgumentParser(

+         description="Processor for PDC unretire packages, handling tickets from '{}'".format(

+             PROJECT_NAMESPACE

+         )

+     )

+ 

+     parser.add_argument(

+         "ticket",

+         type=int,

+         help="Number of ticket to process",

+     )

+ 

+     parser.add_argument(

+         "--config",

+         help="Configuration file",

+     )

+ 

+     parser.add_argument(

+         "--debug",

+         action="store_const",

+         dest="log_level",

+         const=logging.DEBUG,

+         default=logging.INFO,

+         help="Enable debugging output",

+     )

+     return parser.parse_args(args)

+ 

+ 

+ def _setup_logging(log_level: int) -> None:

+     """

+     Setup the logging level.

+ 

+     :arg log_level: Log level to set

+     """

+     handlers = []

+ 

+     _log.setLevel(log_level)

+     # We want all messages logged at level INFO or lower to be printed to stdout

+     info_handler = logging.StreamHandler(stream=sys.stdout)

+     handlers.append(info_handler)

+ 

+     if log_level == logging.INFO:

+         # In normal operation, don't decorate messages

+         for handler in handlers:

+             handler.setFormatter(logging.Formatter("%(message)s"))

+ 

+     logging.basicConfig(level=log_level, handlers=handlers)

+ 

+ 

+ def main(args):

+     """Main function"""

+     args = _get_arguments(args)

+     _setup_logging(log_level=args.log_level)

+     _log.info("hello i'm starting work")

+ 

+     config = toml.load(args.config)

+     parsed_config = config.get("consumer_config", {}).get("default", {})

+     parsed_config.update(

+         config.get("consumer_config", {}).get("pdc_unretire_packages", {})

+     )

+ 

+     ticket = args.ticket

+ 

+     pagure_io = pagure.set_pagure(parsed_config)

+     issue = pagure_io.get_issue(ticket, PROJECT_NAMESPACE)

+ 

+     # Convert issue to message

+     message = IssueNewV1()

+     message.body["issue"] = issue

+     message.body["project"] = {"fullname": PROJECT_NAMESPACE}

+     _log.debug("Message prepared: {}".format(message.body))

+ 

+     PDCUnretirePackages().process(

+         config=parsed_config,

+         message=message,

+     )

+ 

+ 

+ if __name__ == "__main__":

+     try:

+         main(sys.argv[1:])

+     except KeyboardInterrupt:

+         pass

file modified
+48
@@ -62,3 +62,51 @@ 

              result = commits.split("\n")[0]

  

          return result

+ 

+     def get_last_commit_message(self) -> Optional[str]:

+         """

+         Returns the message of the last commit in master branch.

+ 

+         Returns:

+             Commit message.

+         """

+         commit = self.repo.commit()

+ 

+         result = None

+ 

+         if commit:

+             result = commit.message.split("\n")[0]

+ 

+         return result

+ 

+     def get_last_commit_date(self) -> Optional[int]:

+         """

+         Returns the timestamp of the last commit in master branch

+ 

+         Returns:

+             Timestamp of last commit.

+         """

+         commit = self.repo.commit()

+ 

+         result = None

+ 

+         if commit:

+             result = commit.committed_date

+ 

+         return result

+ 

+     def revert_last_commit(self, message: str) -> None:

+         """

+         Revert last commit with message.

+ 

+         Params:

+             message: The message we would like to revert the last commit with.

+         """

+         commit_before_last = self.repo.commit("HEAD~1")

+         commit_before_last_hash = commit_before_last.hexsha

+ 

+         self.repo.git.execute(["git", "checkout", f"{commit_before_last_hash}", "."])

+         self.repo.index.commit(message=message)

+ 

+         origin = self.repo.remote("origin")

+         origin.push()

file modified
+94 -20
@@ -139,7 +139,7 @@ 

          )

  

      def close_issue(

-         self, issue_id: int, namespace: str, message: str, reason: str = "Closed"

+             self, issue_id: int, namespace: str, message: str, reason: str = "Closed"

      ):

          """

          Close the issue defined by the id with provided message and reason.
@@ -308,14 +308,14 @@ 

          )

  

      def new_project(

-         self,

-         namespace: str,

-         repo: str,

-         description: str,

-         upstream_url: str,

-         default_branch: str,

-         initial_commit: bool = False,

-         alias: bool = False,

+             self,

+             namespace: str,

+             repo: str,

+             description: str,

+             upstream_url: str,

+             default_branch: str,

+             initial_commit: bool = False,

+             alias: bool = False,

      ) -> None:

          """

          Create mew project in Pagure.
@@ -433,12 +433,12 @@ 

                      )

  

      def new_branch(

-         self,

-         namespace: str,

-         repo: str,

-         branch: str,

-         from_commit: str = "",

-         from_branch: str = "",

+             self,

+             namespace: str,

+             repo: str,

+             branch: str,

+             from_commit: str = "",

+             from_branch: str = "",

      ) -> None:

          """

          Create a new branch in pagure repository.
@@ -511,10 +511,10 @@ 

              )

  

      def set_monitoring_status(

-         self,

-         namespace: str,

-         repo: str,

-         monitoring_level: str,

+             self,

+             namespace: str,

+             repo: str,

+             monitoring_level: str,

      ) -> None:

          """

          Set a monitoring status for pagure repository. This will work only on dist git.
@@ -573,7 +573,7 @@ 

              )

  

      def change_project_main_admin(

-         self, namespace: str, repo: str, new_main_admin: str

+             self, namespace: str, repo: str, new_main_admin: str

      ) -> None:

          """

          Change the main admin of a project in pagure.
@@ -848,3 +848,77 @@ 

              )

  

          return result

+ 

+     def is_project_orphaned(self, namespace: str, repo: str) -> bool:

+         """

+         Check if project is orphaned.

+         Working only with dist-git

+ 

+         Params:

+             namespace: Namespace of project

+             repo: Name of the project

+ 

+         Returns:

+             Result of check as boolean.

+ 

+         Raises:

+             `toddlers.utils.exceptions.PagureError``: When getting project maintainers fails.

+         """

+         keyword = "orphan"

+ 

+         endpoint_url = "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo

+         headers = self.get_auth_header()

+ 

+         log.debug("Getting project '{0}/{1}' maintainers.")

+         response = self._requests_session.get(endpoint_url, headers=headers)

+ 

+         if response.ok:

+             result = response.json()

+             if result["epel_assignee"] == keyword or result["fedora_assignee"] == keyword:

+                 return True

+             else:

+                 return False

+         else:

+             log.error(

+                 "Error when retrieving project '{0}/{1}' maintainers. "

+                 "Got status_code '{2}'.".format(namespace, repo, response.status_code)

+             )

+             raise PagureError("Couldn't get project '{0}/{1}' maintainers.".format(namespace, repo))

+ 

+     def assign_maintainer_to_project(self, namespace: str, repo: str, maintainer_fas: str) -> None:

+         """

+         Assign maintainer to project.

+         Working only with dist-git

+ 

+         Params:

+             namespace: Namespace of project

+             repo: Name of the project

+             maintainer_fas: FAS of user that will maintain.

+ 

+         Raises:

+             `toddlers.utils.exceptions.PagureError``: When setting project maintainer fails.

+         """

+         endpoint_url = "https://src.fedoraproject.org/_dg/bzoverrides/" + namespace + "/" + repo

+         headers = self.get_auth_header()

+         payload = {"EPEL Maintainer name": maintainer_fas,

+                    "Maintainer name": maintainer_fas}

+ 

+         log.debug(

+             "Setting new maintainer to '{0}' for project '{1}/{2}'".format(

+                 maintainer_fas, namespace, repo

+             )

+         )

+         response = self._requests_session.patch(

+             endpoint_url, data=json.dumps(payload), headers=headers

+         )

+ 

+         if response.status_code != 200:

+             log.error(

+                 "Error when setting new maintainer on project '{0}/{1}'. Got status_code '{2}'."

+                 "".format(

+                     namespace, repo, response.status_code

+                 )

+             )

+             raise PagureError(

+                 "Couldn't set new maintainer on project '{0}/{1}'".format(namespace, repo)

+             )

file modified
+55 -1
@@ -90,7 +90,7 @@ 

  

  

  def new_sla_to_branch(

-     sla_name: str, eol: str, global_component: str, branch: str, branch_type: str

+         sla_name: str, eol: str, global_component: str, branch: str, branch_type: str

  ) -> None:

      """

      Create a new SLA to branch mapping in PDC. Does nothing if SLA already exists.
@@ -221,3 +221,57 @@ 

      # If it doesn't exist create one

      pdc = get_pdc()

      pdc["global-components"]._(payload)

+ 

+ 

+ def get_branch_slas(global_component, component_type, branch):

+     """

+     Getting list of branch SLAs.

+ 

+     Params:

+         global_component: Global component to retrieve

+         component_type: A string Type of component.

+         branch: A string name of branch.

+ 

+     Returns:

+         list representing branch SLA's or None.

+     """

+     query_args = {

+         "global_component": global_component,

+         "branch_type": component_type,

+         "branch": branch,

+     }

+     pdc = get_pdc()

+     query = pdc["component-branch-slas"]._(**query_args)

+ 

+     result = None

+ 

+     if query["count"] != 0:

+         result = query["results"]

+ 

+     return result

+ 

+ 

+ def adjust_eol(global_component, component_type, branch, eol):

+     """

+     Adjusting eol of branch.

+ 

+     Params:

+         global_component: A sting name of the global component

+         component_type: A string Type of component.

+         branch: A string name of branch.

+         eol: A string with date.

+     """

+     existing_branch_slas = get_branch_slas(global_component, component_type, branch)

+ 

+     if existing_branch_slas is None:

+         return

+ 

+     pdc = get_pdc()

+ 

+     payload = {

+         "eol": eol,

+         "branch_active": True

+     }

+ 

+     for branch_sla in existing_branch_slas:

+         pdc["component-branch-slas"][branch_sla["id"]]._(payload)

I added new pdc_unretire_packages plugin.
Methods that are working with git moved to git util.
Methods that are working with pagure moved to pagure util.
Tests for new code in utils.
Optimization pdc_unretire_packages code as a reaction to comments from the previous PR.

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/c9caccbd46124afdbe2649d8f09ecbcc

Not sure if this is typo, but it should probably be PDCUnretirePackages

This will probably don't work well with mypy, because you don't initialize it, just declaring the type. The value will still be None

This will probably don't work well with mypy, because you don't initialize it, just declaring the type. The value will still be None

This will probably don't work well with mypy, because you don't initialize it, just declaring the type. The value will still be None

This will probably don't work well with mypy, because you don't initialize it, just declaring the type. The value will still be None

This will probably don't work well with mypy, because you don't initialize it, just declaring the type. The value will still be None

Don't reformat code you aren't working on.

Don't reformat code you aren't working on.

You are processing Closed tickets when DEVEL is set?

I think the git module already contains clone_repo method, but enhancing it with the error handling would be nice.

This should be configurable, because we want to use staging address on staging. I think it's already in configuration for toddlers.

Check len(list_of_elements) to not crash with index out of bonds.

Add note that this method works only with dist-git.

Add note that this method works only with dist-git.

1 new commit added

  • addressed comments from code review: Fixed typo in Class name, added dist_git_base url from config, removed control for DEVEL during the check if issue is open, handling error which might appear from clone_repo method, removed clean_repo func using TemporaryDir instead, added notes for few methods in pagure.py that they are working just with dist-git, add checking len of elements get_namespace_and_repo method
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/16a4a873b03441a6b33f0d32ae54fb9c

1 new commit added

  • fix: test update - 67% coverage.
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/e29379f4edc84ca59b648507b19395f2

1 new commit added

  • fix: test update - coverage 98% and ref of plugin code
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/d76cf9819893483493b9d2f58f5a7136

1 new commit added

  • fix: error during testing for py38 and py310
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/36c5ae09a48e430db45c2bb9b18d4036

1 new commit added

  • ref: black ref of code
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/5f960d50da034c2b9fc5824cdc869189

1 new commit added

  • feat: pdc functions to adjust eol of branches for specific component and refactoring to match preferred string length.
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/e3924f10239d46258994ff30223c9f45

1 new commit added

  • feat: test for pdc functions
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/928667196b5d4a7cae9768fed13ffd44

1 new commit added

  • test: added tests for new functions in pdc_unretire_packages plugin
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/267808f546b54aa98a102ab70007dc1e

rebased onto 3227491ac6f15b1da32b84c685e0f1cadd1ec027

a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/a246040aa87741a78f45d926c708f171

rebased onto 035f074701115129e0e3ce0f014f1375330690b3

a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/20806c5b90c3456ea074ad1e37db8b92

Pull-Request has been merged by humaton

11 months ago