Skip to content

Commit 66257c5

Browse files
committed
Implement Forgejo event handling in Packit Service.
1 parent 714dcd6 commit 66257c5

File tree

8 files changed

+165
-82
lines changed

8 files changed

+165
-82
lines changed

packit_service/events/forgejo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
from . import abstract, issue, pr, push
55

6-
__all__ = [abstract.__name__, push.__name__, issue.__name__, pr.__name__]
6+
__all__ = [abstract.__name__, issue.__name__, pr.__name__, push.__name__]

packit_service/events/forgejo/issue.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Copyright Contributors to the Packit project.
22
# SPDX-License-Identifier: MIT
33

4-
# SPDX-License-Identifier: MIT
5-
64
from typing import Optional
75

86
from ogr.abstract import Comment as OgrComment

packit_service/events/forgejo/pr.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ def __init__(
2929
actor: str,
3030
body: str,
3131
):
32-
super().__init__(project_url=project_url, pr_id=pr_id, actor=actor)
32+
super().__init__(project_url=project_url, pr_id=pr_id)
3333
self.action = action
3434
self.base_repo_namespace = base_repo_namespace
3535
self.base_repo_name = base_repo_name
3636
self.base_ref = base_ref
3737
self.target_repo_namespace = target_repo_namespace
3838
self.target_repo_name = target_repo_name
39+
self.actor = actor
40+
self.identifier = str(pr_id)
3941
self.commit_sha = commit_sha
42+
self.commit_sha_before = commit_sha_before
4043
self.body = body
4144

4245
def get_dict(self, default_dict: Optional[dict] = None) -> dict:

packit_service/service/api/webhooks.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,9 @@ class ForgejoWebhook(Resource):
373373
@ns.response(HTTPStatus.UNAUTHORIZED.value, "X-Forgejo-Signature validation failed")
374374
@ns.expect(ping_payload_forgejo)
375375
def post(self):
376+
"""
377+
A webhook used by Packit-as-a-Service Forgejo hook.
378+
"""
376379
msg = request.json
377380

378381
if not msg:
@@ -385,17 +388,17 @@ def post(self):
385388
logger.debug(f"/webhooks/forgejo received ping event: {msg['hook']}")
386389
forgejo_webhook_calls.labels(result="pong", process_id=os.getpid()).inc()
387390
return "Pong!", HTTPStatus.OK
388-
# TODO
389-
# try:
390-
# self.validate_token()
391-
# except ValidationFailed as exc:
392-
# logger.info(f"/webhooks/forgejo {exc}")
393-
# forgejo_webhook_calls.labels(
394-
# result="invalid_signature",
395-
# process_id=os.getpid(),
396-
# ).inc()
397-
# return str(exc), HTTPStatus.UNAUTHORIZED
398-
#
391+
392+
try:
393+
self.validate_token()
394+
except ValidationFailed as exc:
395+
logger.info(f"/webhooks/forgejo {exc}")
396+
forgejo_webhook_calls.labels(
397+
result="invalid_signature",
398+
process_id=os.getpid(),
399+
).inc()
400+
return str(exc), HTTPStatus.UNAUTHORIZED
401+
399402
if not self.interested(msg):
400403
forgejo_webhook_calls.labels(
401404
result="not_interested",
@@ -419,39 +422,44 @@ def validate_token(self):
419422
"""
420423
Validate the Forgejo webhook signature.
421424
The signature is a direct SHA256 HMAC hex digest of the raw request body
422-
using the webhook secret as the key, in a similar fashion to Github.
423-
425+
using the webhook secret as the key, in a similar fashion to GitHub.
424426
"""
425427
if "X-Forgejo-Signature" not in request.headers:
428+
if config.validate_webhooks:
429+
msg = "X-Forgejo-Signature not in request.headers"
430+
logger.warning(msg)
431+
raise ValidationFailed(msg)
432+
433+
# don't validate signatures when testing locally
426434
logger.debug("Ain't validating signatures.")
427435
return
428436

429-
if not (webhook_secret := getenv("WEBHOOK_SECRET")):
437+
if not (webhook_secret := config.webhook_secret):
430438
msg = "'webhook_secret' not specified in the config."
431439
logger.error(msg)
432440
raise ValidationFailed(msg)
433441

434-
# Get raw payload
435-
436442
payload = request.get_data()
437443
if not payload:
438444
msg = "No payload received."
439445
logger.error(msg)
440446
raise ValidationFailed(msg)
441447

448+
# Calculate payload signature using HMAC-SHA256
442449
data_hmac = hmac.new(webhook_secret.encode(), msg=payload, digestmod=sha256)
443450
payload_signature = data_hmac.hexdigest()
444-
header_sig = request.headers["X-Forgejo-Signature"]
451+
header_signature = request.headers["X-Forgejo-Signature"]
445452

446-
if header_sig != payload_signature:
453+
if not hmac.compare_digest(header_signature, payload_signature):
447454
msg = "Payload signature validation failed."
448-
449455
logger.warning(msg)
450456
logger.debug(
451-
f"X-Forgejo-Signature: {header_sig!r} != computed: {payload_signature}",
457+
f"X-Forgejo-Signature: {header_signature!r} != computed: {payload_signature}",
452458
)
453459
raise ValidationFailed(msg)
454460

461+
logger.debug("Payload signature is OK.")
462+
455463
@staticmethod
456464
def interested(msg):
457465
"""
@@ -475,7 +483,7 @@ def interested(msg):
475483
"release": action == "published",
476484
"issues": action in {"opened", "edited", "closed", "reopened"},
477485
"issue_comment": action in {"created", "edited"},
478-
"pull_request": action in {"opened", "edited", "closed", "reopened", "synchronize"},
486+
"pull_request": action in {"opened", "reopened", "synchronize"},
479487
}
480488

481489
_interested = interests.get(event or "", False)

packit_service/worker/parser.py

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,48 @@ class Parser:
101101
we need to have method inside the `Parser` class to create objects defined in `event.py`.
102102
"""
103103

104+
@staticmethod
105+
def is_forgejo_event(event: dict) -> bool:
106+
"""
107+
Detect if an event is from Forgejo based on platform-specific fields.
108+
Forgejo events have additional fields that GitHub events don't have.
109+
"""
110+
111+
# Check for Forgejo-specific fields in user objects
112+
def has_forgejo_user_fields(user_obj):
113+
if not isinstance(user_obj, dict):
114+
return False
115+
forgejo_fields = {
116+
"login_name",
117+
"source_id",
118+
"full_name",
119+
"is_admin",
120+
"last_login",
121+
"created",
122+
"restricted",
123+
"active",
124+
"prohibit_login",
125+
"location",
126+
"pronouns",
127+
"website",
128+
"description",
129+
"visibility",
130+
"followers_count",
131+
"following_count",
132+
"starred_repos_count",
133+
"username",
134+
}
135+
return any(field in user_obj for field in forgejo_fields)
136+
137+
return (
138+
has_forgejo_user_fields(event.get("user"))
139+
or has_forgejo_user_fields(nested_get(event, "pull_request", "user"))
140+
or has_forgejo_user_fields(nested_get(event, "comment", "user"))
141+
or has_forgejo_user_fields(nested_get(event, "issue", "user"))
142+
or has_forgejo_user_fields(event.get("pusher"))
143+
or has_forgejo_user_fields(event.get("sender"))
144+
)
145+
104146
@staticmethod
105147
def parse_event(
106148
event: dict,
@@ -159,6 +201,23 @@ def parse_event(
159201
logger.warning("No event to process!")
160202
return None
161203

204+
# Check if this is a Forgejo event and prioritize Forgejo parsers
205+
# We have to prioritize Forgejo events as they are similar in structure
206+
# to github event payloads and hence can accidentally be parsed as
207+
# Github objects.
208+
is_forgejo = Parser.is_forgejo_event(event)
209+
210+
if is_forgejo:
211+
forgejo_parsers = (
212+
Parser.parse_forgejo_push_event,
213+
Parser.parse_forgejo_pr_event,
214+
Parser.parse_forgejo_comment_event,
215+
)
216+
for parser in forgejo_parsers:
217+
forgejo_response = parser(event)
218+
if forgejo_response:
219+
return forgejo_response
220+
162221
for response in (
163222
parser(event)
164223
for parser in (
@@ -191,9 +250,6 @@ def parse_event(
191250
Parser.parse_openscanhub_task_started_event,
192251
Parser.parse_commit_comment_event,
193252
Parser.parse_pagure_pull_request_event,
194-
Parser.parse_forgejo_push_event,
195-
Parser.parse_forgejo_pr_event,
196-
Parser.parse_forgejo_comment_event,
197253
)
198254
):
199255
if response:
@@ -290,11 +346,6 @@ def parse_pr_event(event) -> Optional[github.pr.Action]:
290346
"""Look into the provided event and see if it's one for a new github PR."""
291347
if not event.get("pull_request"):
292348
return None
293-
294-
# Skip Forgejo events - they should be handled by parse_forgejo_pr_event
295-
repository_url = nested_get(event, "repository", "html_url") or ""
296-
if "forgejo.org" in repository_url:
297-
return None
298349

299350
pr_id = event.get("number")
300351
action = event.get("action")
@@ -535,11 +586,6 @@ def parse_github_push_event(event) -> Optional[github.push.Commit]:
535586
"""
536587
Look into the provided event and see if it's one for a new push to the github branch.
537588
"""
538-
# Skip Forgejo events - they should be handled by parse_forgejo_push_event
539-
repository_url = nested_get(event, "repository", "html_url") or ""
540-
if "forgejo.org" in repository_url:
541-
return None
542-
543589
raw_ref = event.get("ref")
544590
before = event.get("before")
545591
pusher = nested_get(event, "pusher", "name")
@@ -608,11 +654,6 @@ def parse_pull_request_comment_event(
608654
# but it's needed when called from parse_event().
609655
if not nested_get(event, "issue", "pull_request"):
610656
return None
611-
612-
# Skip Forgejo events - they should be handled by parse_forgejo_comment_event
613-
repository_url = nested_get(event, "repository", "html_url") or ""
614-
if "forgejo.org" in repository_url:
615-
return None
616657

617658
pr_id = nested_get(event, "issue", "number")
618659
action = event.get("action")
@@ -665,12 +706,6 @@ def parse_issue_comment_event(event) -> Optional[github.issue.Comment]:
665706
# but it's needed when called from parse_event().
666707
if nested_get(event, "issue", "pull_request"):
667708
return None
668-
669-
# Skip Forgejo events - they should be handled by parse_forgejo_comment_event
670-
repository_url = nested_get(event, "repository", "html_url") or ""
671-
if "forgejo.org" in repository_url:
672-
return None
673-
674709
issue_id = nested_get(event, "issue", "number")
675710
action = event.get("action")
676711
if action != "created" or not issue_id:
@@ -1886,7 +1921,7 @@ def parse_forgejo_push_event(event: dict) -> Optional[forgejo.push.Commit]:
18861921
raw_ref = event.get("ref")
18871922
before = event.get("before")
18881923
after = event.get("after")
1889-
pusher = nested_get(event, "pusher", "login") or nested_get(event, "pusher", "name")
1924+
pusher = nested_get(event, "pusher", "login")
18901925

18911926
if not (raw_ref and after and before and pusher):
18921927
return None
@@ -1898,7 +1933,6 @@ def parse_forgejo_push_event(event: dict) -> Optional[forgejo.push.Commit]:
18981933
return None
18991934

19001935
# Number of commits introduced by this push
1901-
commits = event.get("commits") or []
19021936
num_commits = event.get("total_commits")
19031937

19041938
# Strip the ref prefix to get the branch/tag name
@@ -1936,7 +1970,6 @@ def parse_forgejo_pr_event(event: dict) -> Optional[forgejo.pr.Action]:
19361970
Parse Forgejo PR action events, only triggering for relevant actions.
19371971
Supported actions: 'opened', 'reopened', 'synchronize'.
19381972
Skips others like 'closed'.
1939-
19401973
"""
19411974
action_str = event.get("action")
19421975
# Only trigger for these actions
@@ -2011,9 +2044,13 @@ def parse_forgejo_comment_event(
20112044

20122045
if is_pr:
20132046
# For PR comments, extract repo info from pull_request section
2014-
base_repo_namespace = nested_get(event, "pull_request", "head", "repo", "owner", "login")
2047+
base_repo_namespace = nested_get(
2048+
event, "pull_request", "head", "repo", "owner", "login"
2049+
)
20152050
base_repo_name = nested_get(event, "pull_request", "head", "repo", "name")
2016-
target_repo_namespace = nested_get(event, "pull_request", "base", "repo", "owner", "login")
2051+
target_repo_namespace = nested_get(
2052+
event, "pull_request", "base", "repo", "owner", "login"
2053+
)
20172054
else:
20182055
# For issue comments, extract from repository section
20192056
base_repo_namespace = nested_get(event, "repository", "owner", "login")
@@ -2034,10 +2071,12 @@ def parse_forgejo_comment_event(
20342071
return None
20352072

20362073
if is_pr:
2074+
base_ref = nested_get(event, "pull_request", "head", "ref")
2075+
commit_sha = nested_get(event, "pull_request", "head", "sha")
20372076
return forgejo.pr.Comment(
20382077
action=PullRequestCommentAction[action],
20392078
pr_id=issue_id,
2040-
base_ref="",
2079+
base_ref=base_ref,
20412080
base_repo_namespace=base_repo_namespace,
20422081
base_repo_name=base_repo_name,
20432082
target_repo_namespace=target_repo_namespace,
@@ -2046,11 +2085,11 @@ def parse_forgejo_comment_event(
20462085
actor=user_login,
20472086
comment=comment,
20482087
comment_id=comment_id,
2049-
commit_sha=None,
2088+
commit_sha=commit_sha,
20502089
)
20512090
# For issue comments, get the default branch
20522091
default_branch = nested_get(event, "repository", "default_branch") or "main"
2053-
2092+
20542093
return forgejo.issue.Comment(
20552094
action=IssueCommentAction[action],
20562095
issue_id=issue_id,

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pytest
1010
from deepdiff import DeepDiff
1111
from flexmock import flexmock
12-
from ogr import GithubService, GitlabService, PagureService, ForgejoService
12+
from ogr import ForgejoService, GithubService, GitlabService, PagureService
1313
from packit.config import JobConfig, JobConfigTriggerType, PackageConfig
1414
from packit.config.common_package_config import Deployment
1515

@@ -43,7 +43,7 @@ def global_service_config():
4343
GitlabService(token="token"),
4444
PagureService(instance_url="https://src.fedoraproject.org", token="token"),
4545
PagureService(instance_url="https://git.stg.centos.org", token="6789"),
46-
ForgejoService(instance_url="https://codeberg.org", token="token")
46+
ForgejoService(instance_url="https://codeberg.org", token="token"),
4747
}
4848
service_config.server_name = "localhost"
4949
service_config.github_requests_log_path = "/path"

0 commit comments

Comments
 (0)