diff --git a/patchwork/forge/github/__init__.py b/patchwork/forge/github/__init__.py
index 4441c2b..ec9417b 100644
--- a/patchwork/forge/github/__init__.py
+++ b/patchwork/forge/github/__init__.py
@@ -14,9 +14,13 @@ import logging
from patchwork.forge import ForgeBackend
from patchwork.forge import register_backend
+from patchwork.forge.github.to_ml import handle_check_pending
+from patchwork.forge.github.to_ml import handle_check_result
from patchwork.forge.github.to_ml import handle_issue_comment
from patchwork.forge.github.to_ml import handle_pull_request
from patchwork.forge.github.to_ml import handle_review
+from patchwork.forge.github.webhook import parse_check_run
+from patchwork.forge.github.webhook import parse_check_suite
from patchwork.forge.github.webhook import parse_issue_comment
from patchwork.forge.github.webhook import parse_pull_request
from patchwork.forge.github.webhook import parse_review
@@ -44,6 +48,8 @@ class GitHubBackend(ForgeBackend):
payload = json.loads(body)
parsers = {
+ 'check_run': parse_check_run,
+ 'check_suite': parse_check_suite,
'issue_comment': parse_issue_comment,
'pull_request': parse_pull_request,
'pull_request_review': parse_review,
@@ -79,6 +85,8 @@ class GitHubBackend(ForgeBackend):
def process_webhook_event(self, forge_config, event):
handlers = {
+ 'check_pending': handle_check_pending,
+ 'check_result': handle_check_result,
'issue_comment': handle_issue_comment,
'pull_request': handle_pull_request,
'review': handle_review,
diff --git a/patchwork/forge/github/api.py b/patchwork/forge/github/api.py
index 6e99592..36bbab5 100644
--- a/patchwork/forge/github/api.py
+++ b/patchwork/forge/github/api.py
@@ -9,6 +9,7 @@ import logging
import urllib.error
import urllib.request
+from patchwork.forge import CheckRun
from patchwork.forge import ReviewComment
logger = logging.getLogger(__name__)
@@ -64,3 +65,32 @@ def fetch_review_comments(gh, forge_config, pr_number, review_id):
)
)
return comments
+
+
+def fetch_check_runs(gh, forge_config, check_suite_id):
+ """
+ Fetch check runs for a check suite via the GitHub API.
+ """
+ owner, repo = forge_config.repo.split('/', 1)
+ result = gh_api_request(
+ gh,
+ forge_config,
+ 'GET',
+ f'/repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs',
+ )
+ runs = []
+ for run in result.get('check_runs', []):
+ status = run.get('conclusion') or run.get('status', '')
+ desc = ''
+ output = run.get('output')
+ if output:
+ desc = output.get('summary') or ''
+ runs.append(
+ CheckRun(
+ name=run.get('name', ''),
+ status=status,
+ url=run.get('html_url', ''),
+ description=desc,
+ )
+ )
+ return runs
diff --git a/patchwork/forge/github/to_ml.py b/patchwork/forge/github/to_ml.py
index 7129698..c40a6e1 100644
--- a/patchwork/forge/github/to_ml.py
+++ b/patchwork/forge/github/to_ml.py
@@ -6,7 +6,12 @@
import email
import logging
+from django.contrib.auth import get_user_model
+from django.db import transaction
+from django.utils.text import slugify
+
from patchwork.forge.git import GitMirror
+from patchwork.forge.github.api import fetch_check_runs
from patchwork.forge.github.api import fetch_review_comments
from patchwork.forge.util import bytes_to_mbox
from patchwork.forge.util import find_series_by_pr
@@ -16,6 +21,7 @@ from patchwork.forge.util import reply_to_msgid
from patchwork.forge.util import sanitize_pr_body
from patchwork.forge.util import send_emails
from patchwork.forge.util import sender_identity
+from patchwork.models import Check
logger = logging.getLogger(__name__)
@@ -105,6 +111,52 @@ def handle_review(gh, forge_config, event):
reply(gh, forge_config, event, series, subject, body.rstrip())
+def handle_check_pending(gh, forge_config, event):
+ series = find_series_by_pr(gh, forge_config, event.pr_number).last()
+ if not series:
+ return
+
+ create_checks(
+ gh,
+ forge_config,
+ event,
+ event.check_name,
+ Check.STATE_PENDING,
+ event.check_url,
+ '',
+ )
+
+
+def handle_check_result(gh, forge_config, event):
+ series = find_series_by_pr(gh, forge_config, event.pr_number).last()
+ if not series:
+ return
+
+ check_runs = fetch_check_runs(gh, forge_config, event)
+ for run in check_runs:
+ create_checks(
+ series,
+ forge_config,
+ event,
+ run.name,
+ map_check_state(run.status),
+ run.url,
+ run.description,
+ )
+
+ lines = []
+ for run in check_runs:
+ line = f'{run.name} {run.status}'
+ if run.url:
+ line += f': {run.url}'
+ lines.append(line)
+
+ subject = (
+ f'Re: {series.name} (GitHub: {event.check_name} {event.check_status})'
+ )
+ reply(gh, forge_config, event, series, subject, '\n'.join(lines))
+
+
def reply(gh, forge_config, event, series, subject, body):
"""
Build a reply email as an mbox, ingest it into the database and
@@ -136,3 +188,37 @@ def reply(gh, forge_config, event, series, subject, body):
mbox = bytes_to_mbox(msg.as_bytes(unixfrom=True))
ingest_emails(mbox, gh, forge_config, event)
send_emails(mbox, forge_config)
+
+
+def map_check_state(conclusion):
+ """
+ Map a GitHub check conclusion to a patchwork Check state.
+ """
+ state_map = {
+ 'success': Check.STATE_SUCCESS,
+ 'failure': Check.STATE_FAIL,
+ 'timed_out': Check.STATE_FAIL,
+ 'cancelled': Check.STATE_FAIL,
+ 'action_required': Check.STATE_WARNING,
+ }
+ return state_map.get(conclusion, Check.STATE_PENDING)
+
+
+@transaction.atomic
+def create_checks(series, forge_config, event, context, state, url, desc):
+ # Use a dedicated 'github' user so that the checks have proper names.
+ user, _ = get_user_model().objects.get_or_create(
+ username='github',
+ defaults={'is_active': False},
+ )
+ for patch in series.patches.all():
+ # New checks are created even when pending ones complete.
+ # The UI will deduplicate and only display the most recent ones.
+ Check.objects.create(
+ patch=patch,
+ user=user,
+ state=state,
+ target_url=url or '',
+ description=desc or '',
+ context=slugify(context),
+ )
diff --git a/patchwork/forge/github/webhook.py b/patchwork/forge/github/webhook.py
index bc78851..17c5644 100644
--- a/patchwork/forge/github/webhook.py
+++ b/patchwork/forge/github/webhook.py
@@ -75,3 +75,38 @@ def parse_review(payload):
body=review_body,
review_state=review.get('state', ''),
)
+
+
+def parse_check_run(payload):
+ if payload.get('action') != 'created':
+ return None
+ run = payload.get('check_run', {})
+ prs = run.get('pull_requests', [])
+ if not prs:
+ return None
+ return ForgeEvent(
+ type='check_pending',
+ repo_key=get_repo_key(payload),
+ pr_number=prs[0].get('number', 0),
+ check_name=run.get('name', ''),
+ check_status='pending',
+ check_url=run.get('html_url', ''),
+ )
+
+
+def parse_check_suite(payload):
+ if payload.get('action') != 'completed':
+ return None
+ suite = payload.get('check_suite', {})
+ prs = suite.get('pull_requests', [])
+ if not prs:
+ return None
+ app = suite.get('app', {})
+ return ForgeEvent(
+ type='check_result',
+ repo_key=get_repo_key(payload),
+ pr_number=prs[0].get('number', 0),
+ check_suite_id=suite.get('id', 0),
+ check_name=app.get('name', ''),
+ check_status=suite.get('conclusion', ''),
+ )