From 271bba4af57b7b7c6a39dd731c29b76e0788fd93 Mon Sep 17 00:00:00 2001 From: Olamidepeterojo Date: Wed, 19 Mar 2025 17:38:59 +0100 Subject: [PATCH 1/2] implement support for forgejo commit statuses Signed-off-by: Olamidepeterojo --- ogr/services/forgejo/commit_flag.py | 114 ++++++++++++++++++ tests/integration/forgejo/test_commit_flag.py | 96 +++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 ogr/services/forgejo/commit_flag.py create mode 100644 tests/integration/forgejo/test_commit_flag.py diff --git a/ogr/services/forgejo/commit_flag.py b/ogr/services/forgejo/commit_flag.py new file mode 100644 index 00000000..0501ab3e --- /dev/null +++ b/ogr/services/forgejo/commit_flag.py @@ -0,0 +1,114 @@ +# Copyright Contributors to the Packit project. +# SPDX-License-Identifier: MIT + +from datetime import datetime +from typing import Any + +import requests + +from ogr.abstract import CommitFlag, CommitStatus + + +class ForgejoCommitFlag(CommitFlag): + """ + CommitFlag implementation for Forgejo. + """ + + @classmethod + def _state_from_str(cls, state: str) -> CommitStatus: + # Convert a status string returned by Forgejo API into CommitStatus enum. + state = state.lower() + if state == "success": + return CommitStatus.success + if state == "failure": + return CommitStatus.failure + if state == "pending": + return CommitStatus.pending + raise ValueError(f"Unknown commit state from Forgejo: {state}") + + @classmethod + def _validate_state(cls, state: CommitStatus) -> CommitStatus: + # Validate that the provided state is acceptable for Forgejo. + valid_states = { + CommitStatus.success, + CommitStatus.failure, + CommitStatus.pending, + } + if state in valid_states: + return state + raise ValueError(f"Invalid commit state for Forgejo: {state}") + + def _from_raw_commit_flag(self) -> None: + """ + Populate attributes from the raw commit flag data obtained from Forgejo's API. + Expected keys in self._raw_commit_flag: 'commit', 'state', 'context', 'comment', 'id', + 'created', and 'updated'. + """ + raw = self._raw_commit_flag + self.commit = raw.get("commit") + self.state = self._state_from_str(raw.get("state", "pending")) + self.context = raw.get("context") + self.comment = raw.get("comment") + self.uid = raw.get("id") + self.url = raw.get("url") + # Parse timestamps in ISO8601 format (adjust format if needed) + self._created = datetime.strptime(raw.get("created"), "%Y-%m-%dT%H:%M:%SZ") + self._edited = datetime.strptime(raw.get("updated"), "%Y-%m-%dT%H:%M:%SZ") + + @staticmethod + def get(project: Any, commit: str) -> list["CommitFlag"]: + """ + Retrieve commit statuses for the given commit from Forgejo. + This method should use Forgejo's API to fetch statuses. + """ + # Construct the URL using the project's forge_api_url, owner, repo, and commit hash. + url = ( + f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/" + f"{commit}/statuses" + ) + headers = project.get_auth_header() # Get auth headers from project config + response = requests.get(url, headers=headers) + response.raise_for_status() + flags: list[CommitFlag] = [ + ForgejoCommitFlag(raw_commit_flag=raw_flag, project=project, commit=commit) + for raw_flag in response.json() + ] + return flags + + @staticmethod + def set( + project: Any, + commit: str, + state: CommitStatus, + target_url: str, + description: str, + context: str, + ) -> "CommitFlag": + """ + Set a new commit status on Forgejo via its API. + """ + url = ( + f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/" + f"{commit}/statuses" + ) + payload = { + "state": state.name.lower(), + "target_url": target_url, + "description": description, + "context": context, + } + headers = project.get_auth_header() + response = requests.post(url, json=payload, headers=headers) + return ForgejoCommitFlag( + raw_commit_flag=response.json(), + project=project, + commit=commit, + ) + + @property + def created(self) -> datetime: + return self._created + + @property + def edited(self) -> datetime: + return self._edited diff --git a/tests/integration/forgejo/test_commit_flag.py b/tests/integration/forgejo/test_commit_flag.py new file mode 100644 index 00000000..60c1b9a0 --- /dev/null +++ b/tests/integration/forgejo/test_commit_flag.py @@ -0,0 +1,96 @@ +# Copyright Contributors to the Packit project. +# SPDX-License-Identifier: MIT + +import datetime +import responses +import pytest +from typing import Any, Dict, List, Optional +import requests + +from ogr.abstract import CommitStatus, CommitFlag +from ogr.services.forgejo.commit_flag import ForgejoCommitFlag + +class MockProject: + forge_api_url = "http://dummy-forgejo/api/v1" + owner = "dummy_owner" + repo = "dummy_repo" + + def get_auth_header(self) -> Dict[str, str]: + return {"Authorization": "Bearer dummy_token"} + +@responses.activate +def test_get_commit_flag_integration(): + project = MockProject() + commit = "abcdef123456" + url = f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/{commit}/statuses" + + # Dummy response data simulating Forgejo API output. + dummy_response = [{ + "commit": commit, + "state": "success", + "context": "CI", + "comment": "All tests passed", + "id": "123", + "url": "http://dummy-forgejo/commit/abcdef123456/status", + "created": "2023-01-01T12:00:00Z", + "updated": "2023-01-01T12:30:00Z" + }] + responses.add(responses.GET, url, json=dummy_response, status=200) + + # Call the method under test. + flags: List[CommitFlag] = ForgejoCommitFlag.get(project, commit) + + # Assertions using CommitStatus from packit.ogr.abstract. + assert len(flags) == 1 + flag = flags[0] + assert flag.commit == commit + assert flag.state == CommitStatus.success + assert flag.context == "CI" + assert flag.comment == "All tests passed" + assert flag.uid == "123" + + expected_created = datetime.datetime.strptime("2023-01-01T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + expected_updated = datetime.datetime.strptime("2023-01-01T12:30:00Z", "%Y-%m-%dT%H:%M:%SZ") + assert flag.created == expected_created + assert flag.edited == expected_updated + +@responses.activate +def test_set_commit_flag_integration(): + project = MockProject() + commit = "abcdef123456" + url = f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/{commit}/statuses" + + # Dummy response for setting a commit status. + dummy_response = { + "commit": commit, + "state": "success", + "context": "CI", + "comment": "Build succeeded", + "id": "456", + "url": "http://dummy-forgejo/commit/abcdef123456/status", + "created": "2023-02-01T12:00:00Z", + "updated": "2023-02-01T12:30:00Z" + } + responses.add(responses.POST, url, json=dummy_response, status=200) + + # Call the set method to create a new commit flag. + flag = ForgejoCommitFlag.set( + project=project, + commit=commit, + state=CommitStatus.success, + target_url="http://dummy-target", + description="Build succeeded", + context="CI" + ) + + # Assertions to verify correct mapping using CommitStatus. + assert flag.commit == commit + assert flag.state == CommitStatus.success + assert flag.context == "CI" + assert flag.comment == "Build succeeded" + assert flag.uid == "456" + + expected_created = datetime.datetime.strptime("2023-02-01T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ") + expected_updated = datetime.datetime.strptime("2023-02-01T12:30:00Z", "%Y-%m-%dT%H:%M:%SZ") + assert flag.created == expected_created + assert flag.edited == expected_updated From 4a1e96e79aa6df578618181fedef60caa9f01fce Mon Sep 17 00:00:00 2001 From: Olamidepeterojo Date: Thu, 20 Mar 2025 00:19:56 +0100 Subject: [PATCH 2/2] implement support for forgejo commit statuses Signed-off-by: Olamidepeterojo --- ogr/services/forgejo/__init__.py | 2 + ogr/services/forgejo/commit_flag.py | 51 +++--- tests/integration/forgejo/conftest.py | 10 ++ tests/integration/forgejo/test_commit_flag.py | 169 ++++++++++++------ 4 files changed, 152 insertions(+), 80 deletions(-) diff --git a/ogr/services/forgejo/__init__.py b/ogr/services/forgejo/__init__.py index a0e745ed..8fac6216 100644 --- a/ogr/services/forgejo/__init__.py +++ b/ogr/services/forgejo/__init__.py @@ -1,12 +1,14 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT +from ogr.services.forgejo.commit_flag import ForgejoCommitFlag from ogr.services.forgejo.issue import ForgejoIssue from ogr.services.forgejo.project import ForgejoProject from ogr.services.forgejo.pull_request import ForgejoPullRequest from ogr.services.forgejo.service import ForgejoService __all__ = [ + ForgejoCommitFlag.__name__, ForgejoPullRequest.__name__, ForgejoIssue.__name__, ForgejoProject.__name__, diff --git a/ogr/services/forgejo/commit_flag.py b/ogr/services/forgejo/commit_flag.py index 0501ab3e..a22e6468 100644 --- a/ogr/services/forgejo/commit_flag.py +++ b/ogr/services/forgejo/commit_flag.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Any -import requests +from pyforgejo import PyforgejoApi from ogr.abstract import CommitFlag, CommitStatus @@ -51,29 +51,24 @@ def _from_raw_commit_flag(self) -> None: self.comment = raw.get("comment") self.uid = raw.get("id") self.url = raw.get("url") - # Parse timestamps in ISO8601 format (adjust format if needed) - self._created = datetime.strptime(raw.get("created"), "%Y-%m-%dT%H:%M:%SZ") - self._edited = datetime.strptime(raw.get("updated"), "%Y-%m-%dT%H:%M:%SZ") + self._created = raw.get("created") + self._edited = raw.get("updated") @staticmethod def get(project: Any, commit: str) -> list["CommitFlag"]: """ - Retrieve commit statuses for the given commit from Forgejo. - This method should use Forgejo's API to fetch statuses. + Retrieve commit statuses for the given commit from Forgejo using the pyforgejo SDK. """ - # Construct the URL using the project's forge_api_url, owner, repo, and commit hash. - url = ( - f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/" - f"{commit}/statuses" + client = PyforgejoApi(api_url=project.forge_api_url, token=project.token) + raw_flags = client.get_commit_statuses( + owner=project.owner, + repo=project.repo, + commit=commit, ) - headers = project.get_auth_header() # Get auth headers from project config - response = requests.get(url, headers=headers) - response.raise_for_status() - flags: list[CommitFlag] = [ + return [ ForgejoCommitFlag(raw_commit_flag=raw_flag, project=project, commit=commit) - for raw_flag in response.json() + for raw_flag in raw_flags ] - return flags @staticmethod def set( @@ -85,22 +80,20 @@ def set( context: str, ) -> "CommitFlag": """ - Set a new commit status on Forgejo via its API. + Set a new commit status on Forgejo via the pyforgejo SDK. """ - url = ( - f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/" - f"{commit}/statuses" + client = PyforgejoApi(api_url=project.forge_api_url, token=project.token) + raw_response = client.set_commit_status( + owner=project.owner, + repo=project.repo, + commit=commit, + state=state.name.lower(), + target_url=target_url, + description=description, + context=context, ) - payload = { - "state": state.name.lower(), - "target_url": target_url, - "description": description, - "context": context, - } - headers = project.get_auth_header() - response = requests.post(url, json=payload, headers=headers) return ForgejoCommitFlag( - raw_commit_flag=response.json(), + raw_commit_flag=raw_response, project=project, commit=commit, ) diff --git a/tests/integration/forgejo/conftest.py b/tests/integration/forgejo/conftest.py index e254c700..449749aa 100644 --- a/tests/integration/forgejo/conftest.py +++ b/tests/integration/forgejo/conftest.py @@ -15,3 +15,13 @@ def service(): instance_url="https://v10.next.forgejo.org", api_key=api_key, ) + + +@pytest.fixture +def project(service): + repo = os.environ.get("FORGEJO_REPO", "existing_repo_name") + namespace = os.environ.get("FORGEJO_NAMESPACE", "existing_namespace") + project = service.get_project(repo=repo, namespace=namespace) + if not project: + pytest.skip(f"Project {namespace}/{repo} does not exist.") + return project diff --git a/tests/integration/forgejo/test_commit_flag.py b/tests/integration/forgejo/test_commit_flag.py index 60c1b9a0..d6ddfde0 100644 --- a/tests/integration/forgejo/test_commit_flag.py +++ b/tests/integration/forgejo/test_commit_flag.py @@ -2,45 +2,116 @@ # SPDX-License-Identifier: MIT import datetime -import responses + import pytest -from typing import Any, Dict, List, Optional -import requests -from ogr.abstract import CommitStatus, CommitFlag +from ogr.abstract import CommitFlag, CommitStatus from ogr.services.forgejo.commit_flag import ForgejoCommitFlag + +def fake_get_commit_statuses(self, owner, repo, commit): + return [ + { + "commit": commit, + "state": "success", + "context": "CI", + "comment": "All tests passed", + "id": "123", + "url": f"http://dummy-forgejo/commit/{commit}/status", + "created": datetime.datetime.strptime( + "2023-01-01T12:00:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ), + "updated": datetime.datetime.strptime( + "2023-01-01T12:30:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ), + }, + ] + + +def fake_set_commit_status( + self, + owner, + repo, + commit, + state, + target_url, + description, + context, +): + return { + "commit": commit, + "state": state, + "context": context, + "comment": description, + "id": "456", + "url": f"http://dummy-forgejo/commit/{commit}/status", + "created": datetime.datetime.strptime( + "2023-02-01T12:00:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ), + "updated": datetime.datetime.strptime( + "2023-02-01T12:30:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ), + } + + +class FakePyforgejoApi: + def __init__(self, api_url, token): + self.api_url = api_url + self.token = token + + def get_commit_statuses(self, owner, repo, commit): + return fake_get_commit_statuses(self, owner, repo, commit) + + def set_commit_status( + self, + owner, + repo, + commit, + state, + target_url, + description, + context, + ): + return fake_set_commit_status( + self, + owner, + repo, + commit, + state, + target_url, + description, + context, + ) + + class MockProject: forge_api_url = "http://dummy-forgejo/api/v1" owner = "dummy_owner" repo = "dummy_repo" + token = "dummy_token" - def get_auth_header(self) -> Dict[str, str]: + def get_auth_header(self) -> dict[str, str]: return {"Authorization": "Bearer dummy_token"} -@responses.activate + +@pytest.fixture(autouse=True) +def patch_pyforgejo_api(monkeypatch): + monkeypatch.setattr( + "ogr.services.forgejo.commit_flag.PyforgejoApi", + FakePyforgejoApi, + ) + + def test_get_commit_flag_integration(): project = MockProject() commit = "abcdef123456" - url = f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/{commit}/statuses" - - # Dummy response data simulating Forgejo API output. - dummy_response = [{ - "commit": commit, - "state": "success", - "context": "CI", - "comment": "All tests passed", - "id": "123", - "url": "http://dummy-forgejo/commit/abcdef123456/status", - "created": "2023-01-01T12:00:00Z", - "updated": "2023-01-01T12:30:00Z" - }] - responses.add(responses.GET, url, json=dummy_response, status=200) - - # Call the method under test. - flags: List[CommitFlag] = ForgejoCommitFlag.get(project, commit) - - # Assertions using CommitStatus from packit.ogr.abstract. + + flags: list[CommitFlag] = ForgejoCommitFlag.get(project, commit) + assert len(flags) == 1 flag = flags[0] assert flag.commit == commit @@ -48,49 +119,45 @@ def test_get_commit_flag_integration(): assert flag.context == "CI" assert flag.comment == "All tests passed" assert flag.uid == "123" - - expected_created = datetime.datetime.strptime("2023-01-01T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ") - expected_updated = datetime.datetime.strptime("2023-01-01T12:30:00Z", "%Y-%m-%dT%H:%M:%SZ") + + expected_created = datetime.datetime.strptime( + "2023-01-01T12:00:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ) + expected_updated = datetime.datetime.strptime( + "2023-01-01T12:30:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ) assert flag.created == expected_created assert flag.edited == expected_updated -@responses.activate + def test_set_commit_flag_integration(): project = MockProject() commit = "abcdef123456" - url = f"{project.forge_api_url}/repos/{project.owner}/{project.repo}/commits/{commit}/statuses" - - # Dummy response for setting a commit status. - dummy_response = { - "commit": commit, - "state": "success", - "context": "CI", - "comment": "Build succeeded", - "id": "456", - "url": "http://dummy-forgejo/commit/abcdef123456/status", - "created": "2023-02-01T12:00:00Z", - "updated": "2023-02-01T12:30:00Z" - } - responses.add(responses.POST, url, json=dummy_response, status=200) - - # Call the set method to create a new commit flag. + flag = ForgejoCommitFlag.set( project=project, commit=commit, state=CommitStatus.success, target_url="http://dummy-target", description="Build succeeded", - context="CI" + context="CI", ) - - # Assertions to verify correct mapping using CommitStatus. + assert flag.commit == commit assert flag.state == CommitStatus.success assert flag.context == "CI" assert flag.comment == "Build succeeded" assert flag.uid == "456" - - expected_created = datetime.datetime.strptime("2023-02-01T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ") - expected_updated = datetime.datetime.strptime("2023-02-01T12:30:00Z", "%Y-%m-%dT%H:%M:%SZ") + + expected_created = datetime.datetime.strptime( + "2023-02-01T12:00:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ) + expected_updated = datetime.datetime.strptime( + "2023-02-01T12:30:00Z", + "%Y-%m-%dT%H:%M:%SZ", + ) assert flag.created == expected_created assert flag.edited == expected_updated