Merge "Implement basic github checks API workflow"
This commit is contained in:
commit
bdda719298
|
@ -59,6 +59,7 @@ To create a `GitHub application
|
||||||
* Set permissions:
|
* Set permissions:
|
||||||
|
|
||||||
* Repository administration: Read
|
* Repository administration: Read
|
||||||
|
* Checks: Read & Write
|
||||||
* Repository contents: Read & Write (write to let zuul merge change)
|
* Repository contents: Read & Write (write to let zuul merge change)
|
||||||
* Issues: Read & Write
|
* Issues: Read & Write
|
||||||
* Pull requests: Read & Write
|
* Pull requests: Read & Write
|
||||||
|
@ -66,6 +67,7 @@ To create a `GitHub application
|
||||||
|
|
||||||
* Set events subscription:
|
* Set events subscription:
|
||||||
|
|
||||||
|
* Check run
|
||||||
* Commit comment
|
* Commit comment
|
||||||
* Create
|
* Create
|
||||||
* Push
|
* Push
|
||||||
|
@ -203,6 +205,8 @@ the following options.
|
||||||
|
|
||||||
.. value:: push
|
.. value:: push
|
||||||
|
|
||||||
|
.. value:: check_run
|
||||||
|
|
||||||
.. attr:: action
|
.. attr:: action
|
||||||
|
|
||||||
A :value:`pipeline.trigger.<github source>.event.pull_request`
|
A :value:`pipeline.trigger.<github source>.event.pull_request`
|
||||||
|
@ -254,6 +258,18 @@ the following options.
|
||||||
|
|
||||||
Pull request review removed.
|
Pull request review removed.
|
||||||
|
|
||||||
|
A :value:`pipeline.trigger.<github source>.event.check_run`
|
||||||
|
event will have associated action(s) to trigger from. The
|
||||||
|
supported actions are:
|
||||||
|
|
||||||
|
.. value:: requested
|
||||||
|
|
||||||
|
A check run is requested.
|
||||||
|
|
||||||
|
.. value:: completed
|
||||||
|
|
||||||
|
A check run completed.
|
||||||
|
|
||||||
.. attr:: branch
|
.. attr:: branch
|
||||||
|
|
||||||
The branch associated with the event. Example: ``master``. This
|
The branch associated with the event. Example: ``master``. This
|
||||||
|
@ -295,6 +311,23 @@ the following options.
|
||||||
format of ``user:context:status``. For example,
|
format of ``user:context:status``. For example,
|
||||||
``zuul_github_ci_bot:check_pipeline:success``.
|
``zuul_github_ci_bot:check_pipeline:success``.
|
||||||
|
|
||||||
|
.. attr: check
|
||||||
|
|
||||||
|
This is only used for ``check_run`` events. It works similar to
|
||||||
|
the ``status`` attribute and accepts a list of strings each of
|
||||||
|
which matches the app requesting or updating the check run, the
|
||||||
|
check run's name and the conclusion in the format of
|
||||||
|
``app:name::conclusion``.
|
||||||
|
To make Zuul properly interact with Github's checks API, each
|
||||||
|
pipeline that is using the checks API should have at least one
|
||||||
|
trigger that matches the pipeline's name regardless of the result,
|
||||||
|
e.g. ``zuul:cool-pipeline:.*``. This will enable the cool-pipeline
|
||||||
|
to trigger whenever a user requests the ``cool-pipeline`` check
|
||||||
|
run as part of the ``zuul`` check suite.
|
||||||
|
Additionally, one could use ``.*:success`` to trigger a pipeline
|
||||||
|
whenever a successful check run is reported (e.g. useful for
|
||||||
|
gating).
|
||||||
|
|
||||||
.. attr:: ref
|
.. attr:: ref
|
||||||
|
|
||||||
This is only used for ``push`` events. This field is treated as
|
This is only used for ``push`` events. This field is treated as
|
||||||
|
@ -330,6 +363,12 @@ itself. Status name, description, and context is taken from the pipeline.
|
||||||
status. Defaults to the zuul server status_url, or the empty
|
status. Defaults to the zuul server status_url, or the empty
|
||||||
string if that is unset.
|
string if that is unset.
|
||||||
|
|
||||||
|
.. attr:: check
|
||||||
|
|
||||||
|
If the reporter should utilize github's checks API to set the commit
|
||||||
|
status, this must be set to ``in_progress``, ``success`` or ``failure``
|
||||||
|
(depending on which status the reporter should report).
|
||||||
|
|
||||||
.. attr:: comment
|
.. attr:: comment
|
||||||
:default: true
|
:default: true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
The Github driver now has a basic support for the Github checks API.
|
||||||
|
To enable reporting build results via the checks API one can configure the
|
||||||
|
the new :attr:`pipeline.<reporter>.<github source>.check` attribute on the
|
||||||
|
Github reporter. It's also possible to trigger on a requested or completed
|
||||||
|
:value:`pipeline.trigger.<github source>.event.check_run`.
|
||||||
|
|
||||||
|
To be able to use the checks API, zuul must be authenticated as Github
|
||||||
|
app. For more information about the necessary requirements, please see
|
||||||
|
the :ref:`github_driver` driver documentation.
|
|
@ -2089,6 +2089,23 @@ class FakeGithubPullRequest(object):
|
||||||
}
|
}
|
||||||
return (name, data)
|
return (name, data)
|
||||||
|
|
||||||
|
def getCheckRunRequestedEvent(self, cr_name, app="zuul"):
|
||||||
|
name = "check_run"
|
||||||
|
data = {
|
||||||
|
"action": "rerequested",
|
||||||
|
"check_run": {
|
||||||
|
"head_sha": self.head_sha,
|
||||||
|
"name": cr_name,
|
||||||
|
"app": {
|
||||||
|
"slug": app,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"full_name": self.project,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return (name, data)
|
||||||
|
|
||||||
def setMerged(self, commit_message):
|
def setMerged(self, commit_message):
|
||||||
self.is_merged = True
|
self.is_merged = True
|
||||||
self.merge_message = commit_message
|
self.merge_message = commit_message
|
||||||
|
|
|
@ -64,6 +64,47 @@ class FakeStatus(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCheckRun(object):
|
||||||
|
def __init__(self, name, details_url, output, status, conclusion,
|
||||||
|
completed_at, app):
|
||||||
|
self.name = name
|
||||||
|
self.details_url = details_url
|
||||||
|
self.output = output
|
||||||
|
self.conclusion = conclusion
|
||||||
|
self.completed_at = completed_at
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
# Github automatically sets the status to "completed" if a conclusion
|
||||||
|
# is provided.
|
||||||
|
if conclusion is not None:
|
||||||
|
self.status = "completed"
|
||||||
|
else:
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"status": self.status,
|
||||||
|
"output": self.output,
|
||||||
|
"details_url": self.details_url,
|
||||||
|
"conclusion": self.conclusion,
|
||||||
|
"completed_at": self.completed_at,
|
||||||
|
"app": {
|
||||||
|
"slug": self.app,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, conclusion, completed_at, output, details_url):
|
||||||
|
self.conclusion = conclusion
|
||||||
|
self.completed_at = completed_at
|
||||||
|
self.output = output
|
||||||
|
self.details_url = details_url
|
||||||
|
|
||||||
|
# As we are only calling the update method when a build is completed,
|
||||||
|
# we can always set the status to "completed".
|
||||||
|
self.status = "completed"
|
||||||
|
|
||||||
|
|
||||||
class FakeGHReview(object):
|
class FakeGHReview(object):
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
@ -83,6 +124,7 @@ class FakeCommit(object):
|
||||||
def __init__(self, sha):
|
def __init__(self, sha):
|
||||||
self._statuses = []
|
self._statuses = []
|
||||||
self.sha = sha
|
self.sha = sha
|
||||||
|
self._check_runs = []
|
||||||
|
|
||||||
def set_status(self, state, url, description, context, user):
|
def set_status(self, state, url, description, context, user):
|
||||||
status = FakeStatus(
|
status = FakeStatus(
|
||||||
|
@ -91,14 +133,29 @@ class FakeCommit(object):
|
||||||
# the last status provided for a commit.
|
# the last status provided for a commit.
|
||||||
self._statuses.insert(0, status)
|
self._statuses.insert(0, status)
|
||||||
|
|
||||||
|
def set_check_run(self, name, details_url, output, status, conclusion,
|
||||||
|
completed_at, app):
|
||||||
|
check_run = FakeCheckRun(
|
||||||
|
name, details_url, output, status, conclusion, completed_at, app)
|
||||||
|
# Always insert a check_run to the front of the list to represent the
|
||||||
|
# last check_run provided for a commit.
|
||||||
|
self._check_runs.insert(0, check_run)
|
||||||
|
|
||||||
def get_url(self, path, params=None):
|
def get_url(self, path, params=None):
|
||||||
if path == 'statuses':
|
if path == 'statuses':
|
||||||
statuses = [s.as_dict() for s in self._statuses]
|
statuses = [s.as_dict() for s in self._statuses]
|
||||||
return FakeResponse(statuses)
|
return FakeResponse(statuses)
|
||||||
|
if path == "check-runs":
|
||||||
|
check_runs = [c.as_dict() for c in self._check_runs]
|
||||||
|
resp = {"total_count": len(check_runs), "check_runs": check_runs}
|
||||||
|
return FakeResponse(resp)
|
||||||
|
|
||||||
def statuses(self):
|
def statuses(self):
|
||||||
return self._statuses
|
return self._statuses
|
||||||
|
|
||||||
|
def check_runs(self):
|
||||||
|
return self._check_runs
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
'''
|
'''
|
||||||
Returns the combined status wich only contains the latest statuses of
|
Returns the combined status wich only contains the latest statuses of
|
||||||
|
@ -121,6 +178,15 @@ class FakeRepository(object):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
# Simple dictionary to store permission values per feature (e.g.
|
||||||
|
# checks, Repository contents, Pull requests, Commit statuses, ...).
|
||||||
|
# Could be used to just enable/deable a permission (True, False) or
|
||||||
|
# provide more specific values like "read" or "read&write". The mocked
|
||||||
|
# functionality in the FakeRepository class should then check for this
|
||||||
|
# value and raise an appropriate exception like a production Github
|
||||||
|
# would do in case the permission is not sufficient or missing at all.
|
||||||
|
self._permissions = {}
|
||||||
|
|
||||||
# fail the next commit requests with 404
|
# fail the next commit requests with 404
|
||||||
self.fail_not_found = 0
|
self.fail_not_found = 0
|
||||||
|
|
||||||
|
@ -136,6 +202,13 @@ class FakeRepository(object):
|
||||||
branch.protected = protected
|
branch.protected = protected
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _set_permission(self, key, value):
|
||||||
|
# NOTE (felix): Currently, this is only used to mock a repo with
|
||||||
|
# missing checks API permissions. But we could also use it to test
|
||||||
|
# arbitrary permission values like missing write, but only read
|
||||||
|
# permissions for a specific functionality.
|
||||||
|
self._permissions[key] = value
|
||||||
|
|
||||||
def _build_url(self, *args, **kwargs):
|
def _build_url(self, *args, **kwargs):
|
||||||
path_args = ['repos', self.name]
|
path_args = ['repos', self.name]
|
||||||
path_args.extend(args)
|
path_args.extend(args)
|
||||||
|
@ -162,6 +235,26 @@ class FakeRepository(object):
|
||||||
self._commits[sha] = commit
|
self._commits[sha] = commit
|
||||||
commit.set_status(state, url, description, context, user)
|
commit.set_status(state, url, description, context, user)
|
||||||
|
|
||||||
|
def create_check_run(self, head_sha, name, details_url=None, output=None,
|
||||||
|
status=None, conclusion=None, completed_at=None,
|
||||||
|
app="zuul"):
|
||||||
|
|
||||||
|
# Raise the appropriate github3 exception in case we don't have
|
||||||
|
# permission to access the checks API
|
||||||
|
if self._permissions.get("checks") is False:
|
||||||
|
# To create a proper github3 exception, we need to mock a response
|
||||||
|
# object
|
||||||
|
raise github3.exceptions.ForbiddenError(
|
||||||
|
FakeResponse("Resource not accessible by integration", 403)
|
||||||
|
)
|
||||||
|
|
||||||
|
commit = self._commits.get(head_sha, None)
|
||||||
|
if commit is None:
|
||||||
|
commit = FakeCommit(head_sha)
|
||||||
|
self._commits[head_sha] = commit
|
||||||
|
commit.set_check_run(
|
||||||
|
name, details_url, output, status, conclusion, completed_at, app)
|
||||||
|
|
||||||
def commit(self, sha):
|
def commit(self, sha):
|
||||||
|
|
||||||
if self.fail_not_found > 0:
|
if self.fail_not_found > 0:
|
||||||
|
@ -410,6 +503,12 @@ class FakeResponse(object):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.links = {}
|
self.links = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self):
|
||||||
|
# Building github3 exceptions requires a Response object with the
|
||||||
|
# content attribute set.
|
||||||
|
return self.data
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,25 @@
|
||||||
comment: true
|
comment: true
|
||||||
status: failure
|
status: failure
|
||||||
|
|
||||||
|
- pipeline:
|
||||||
|
name: checks-api-reporting
|
||||||
|
description: Reporting via Githubs Checks API
|
||||||
|
manager: independent
|
||||||
|
trigger:
|
||||||
|
github:
|
||||||
|
- event: push
|
||||||
|
- event: pull_request
|
||||||
|
action: opened
|
||||||
|
start:
|
||||||
|
github:
|
||||||
|
check: in_progress
|
||||||
|
success:
|
||||||
|
github:
|
||||||
|
check: success
|
||||||
|
failure:
|
||||||
|
github:
|
||||||
|
check: failure
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
name: base
|
name: base
|
||||||
parent: null
|
parent: null
|
||||||
|
@ -104,3 +123,9 @@
|
||||||
push-reporting:
|
push-reporting:
|
||||||
jobs:
|
jobs:
|
||||||
- project-test1
|
- project-test1
|
||||||
|
|
||||||
|
- project:
|
||||||
|
name: org/project3
|
||||||
|
checks-api-reporting:
|
||||||
|
jobs:
|
||||||
|
- project-test1
|
||||||
|
|
|
@ -75,6 +75,19 @@
|
||||||
github:
|
github:
|
||||||
comment: true
|
comment: true
|
||||||
|
|
||||||
|
- pipeline:
|
||||||
|
name: trigger_check_run
|
||||||
|
manager: independent
|
||||||
|
trigger:
|
||||||
|
github:
|
||||||
|
- event: check_run
|
||||||
|
action: requested
|
||||||
|
check: zuul:tenant-one/check:.*
|
||||||
|
success:
|
||||||
|
github:
|
||||||
|
comment: true
|
||||||
|
check: success
|
||||||
|
|
||||||
- pipeline:
|
- pipeline:
|
||||||
name: trigger
|
name: trigger
|
||||||
manager: independent
|
manager: independent
|
||||||
|
@ -350,6 +363,10 @@
|
||||||
name: project14-current
|
name: project14-current
|
||||||
run: playbooks/project14-current.yaml
|
run: playbooks/project14-current.yaml
|
||||||
|
|
||||||
|
- job:
|
||||||
|
name: project15-check-run
|
||||||
|
run: playbooks/project15-check-run.yaml
|
||||||
|
|
||||||
- project:
|
- project:
|
||||||
name: org/project1
|
name: org/project1
|
||||||
pipeline:
|
pipeline:
|
||||||
|
@ -442,3 +459,9 @@
|
||||||
reject_current:
|
reject_current:
|
||||||
jobs:
|
jobs:
|
||||||
- project14-current
|
- project14-current
|
||||||
|
|
||||||
|
- project:
|
||||||
|
name: org/project15
|
||||||
|
trigger_check_run:
|
||||||
|
jobs:
|
||||||
|
- project15-check-run
|
||||||
|
|
|
@ -26,7 +26,8 @@ import github3.exceptions
|
||||||
from zuul.driver.github.githubconnection import GithubShaCache
|
from zuul.driver.github.githubconnection import GithubShaCache
|
||||||
import zuul.rpcclient
|
import zuul.rpcclient
|
||||||
|
|
||||||
from tests.base import BaseTestCase, ZuulTestCase, simple_layout, random_sha1
|
from tests.base import (BaseTestCase, ZuulGithubAppTestCase, ZuulTestCase,
|
||||||
|
simple_layout, random_sha1)
|
||||||
from tests.base import ZuulWebFixture
|
from tests.base import ZuulWebFixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -576,6 +577,37 @@ class TestGithubDriver(ZuulTestCase):
|
||||||
self.executor_server.release()
|
self.executor_server.release()
|
||||||
self.waitUntilSettled()
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
@simple_layout("layouts/reporting-github.yaml", driver="github")
|
||||||
|
def test_reporting_checks_api_unauthorized(self):
|
||||||
|
# Using the checks API only works with app authentication. As all tests
|
||||||
|
# within the TestGithubDriver class are executed without app
|
||||||
|
# authentication, the checks API won't work here.
|
||||||
|
|
||||||
|
project = "org/project3"
|
||||||
|
github = self.fake_github.getGithubClient(None)
|
||||||
|
|
||||||
|
# The pipeline reports pull request status both on start and success.
|
||||||
|
# As we are not authenticated as app, this won't create or update any
|
||||||
|
# check runs, but should post two comments (start, success) informing
|
||||||
|
# the user about the missing authentication.
|
||||||
|
A = self.fake_github.openFakePullRequest(project, "master", "A")
|
||||||
|
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
A.head_sha, github.repo_from_project(project)._commits.keys()
|
||||||
|
)
|
||||||
|
check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
|
||||||
|
self.assertEqual(0, len(check_runs))
|
||||||
|
|
||||||
|
expected_warning = (
|
||||||
|
"Unable to create or update check tenant-one/checks-api-reporting."
|
||||||
|
" Must be authenticated as app integration."
|
||||||
|
)
|
||||||
|
self.assertEqual(2, len(A.comments))
|
||||||
|
self.assertIn(expected_warning, A.comments[0])
|
||||||
|
self.assertIn(expected_warning, A.comments[1])
|
||||||
|
|
||||||
@simple_layout('layouts/merging-github.yaml', driver='github')
|
@simple_layout('layouts/merging-github.yaml', driver='github')
|
||||||
def test_report_pull_merge(self):
|
def test_report_pull_merge(self):
|
||||||
# pipeline merges the pull request on success
|
# pipeline merges the pull request on success
|
||||||
|
@ -1554,3 +1586,128 @@ class TestGithubShaCache(BaseTestCase):
|
||||||
}
|
}
|
||||||
cache.update('foo/bar', pr_dict)
|
cache.update('foo/bar', pr_dict)
|
||||||
self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
|
self.assertEqual(cache.get('foo/bar', '123456'), set({1}))
|
||||||
|
|
||||||
|
|
||||||
|
class TestGithubAppDriver(ZuulGithubAppTestCase):
|
||||||
|
"""Inheriting from ZuulGithubAppTestCase will enable app authentication"""
|
||||||
|
config_file = 'zuul-github-driver.conf'
|
||||||
|
|
||||||
|
@simple_layout("layouts/reporting-github.yaml", driver="github")
|
||||||
|
def test_reporting_checks_api(self):
|
||||||
|
"""Using the checks API only works with app authentication"""
|
||||||
|
project = "org/project3"
|
||||||
|
github = self.fake_github.getGithubClient(None)
|
||||||
|
|
||||||
|
# pipeline reports pull request status both on start and success
|
||||||
|
self.executor_server.hold_jobs_in_build = True
|
||||||
|
A = self.fake_github.openFakePullRequest(project, "master", "A")
|
||||||
|
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
# We should have a pending check for the head sha
|
||||||
|
self.assertIn(
|
||||||
|
A.head_sha, github.repo_from_project(project)._commits.keys())
|
||||||
|
check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(check_runs))
|
||||||
|
check_run = check_runs[0]
|
||||||
|
|
||||||
|
self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
|
||||||
|
self.assertEqual("in_progress", check_run["status"])
|
||||||
|
self.assertThat(
|
||||||
|
check_run["output"]["summary"],
|
||||||
|
MatchesRegex(r'.*Starting checks-api-reporting jobs.*', re.DOTALL)
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO (felix): How can we test if the details_url was set correctly?
|
||||||
|
# How can the details_url be configured on the test case?
|
||||||
|
|
||||||
|
self.executor_server.hold_jobs_in_build = False
|
||||||
|
self.executor_server.release()
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
# We should now have an updated status for the head sha
|
||||||
|
check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
|
||||||
|
self.assertEqual(1, len(check_runs))
|
||||||
|
check_run = check_runs[0]
|
||||||
|
|
||||||
|
self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
|
||||||
|
self.assertEqual("completed", check_run["status"])
|
||||||
|
self.assertEqual("success", check_run["conclusion"])
|
||||||
|
self.assertThat(
|
||||||
|
check_run["output"]["summary"],
|
||||||
|
MatchesRegex(r'.*Build succeeded.*', re.DOTALL)
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(check_run["completed_at"])
|
||||||
|
|
||||||
|
@simple_layout("layouts/reporting-github.yaml", driver="github")
|
||||||
|
def test_update_non_existing_check_run(self):
|
||||||
|
project = "org/project3"
|
||||||
|
github = self.fake_github.getGithubClient(None)
|
||||||
|
|
||||||
|
# pipeline reports pull request status both on start and success
|
||||||
|
self.executor_server.hold_jobs_in_build = True
|
||||||
|
A = self.fake_github.openFakePullRequest(project, "master", "A")
|
||||||
|
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
# We should have a pending check for the head sha
|
||||||
|
commit = github.repo_from_project(project)._commits.get(A.head_sha)
|
||||||
|
check_runs = commit.check_runs()
|
||||||
|
self.assertEqual(1, len(check_runs))
|
||||||
|
|
||||||
|
# Delete this check_run to simulate a failed check_run creation
|
||||||
|
commit._check_runs = []
|
||||||
|
|
||||||
|
# Now run the build and check if the update of the check_run could
|
||||||
|
# still be accomplished.
|
||||||
|
self.executor_server.hold_jobs_in_build = False
|
||||||
|
self.executor_server.release()
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
|
||||||
|
self.assertEqual(1, len(check_runs))
|
||||||
|
check_run = check_runs[0]
|
||||||
|
|
||||||
|
self.assertEqual("tenant-one/checks-api-reporting", check_run["name"])
|
||||||
|
self.assertEqual("completed", check_run["status"])
|
||||||
|
self.assertEqual("success", check_run["conclusion"])
|
||||||
|
self.assertThat(
|
||||||
|
check_run["output"]["summary"],
|
||||||
|
MatchesRegex(r'.*Build succeeded.*', re.DOTALL)
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(check_run["completed_at"])
|
||||||
|
|
||||||
|
@simple_layout("layouts/reporting-github.yaml", driver="github")
|
||||||
|
def test_update_check_run_missing_permissions(self):
|
||||||
|
project = "org/project3"
|
||||||
|
github = self.fake_github.getGithubClient(None)
|
||||||
|
|
||||||
|
repo = github.repo_from_project(project)
|
||||||
|
repo._set_permission("checks", False)
|
||||||
|
|
||||||
|
A = self.fake_github.openFakePullRequest(project, "master", "A")
|
||||||
|
self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
|
||||||
|
self.waitUntilSettled()
|
||||||
|
|
||||||
|
# Alghough we are authenticated as github app, we are lacking the
|
||||||
|
# necessary "checks" permissions for the test repository. Thus, the
|
||||||
|
# check run creation/update should fail and we end up in two comments
|
||||||
|
# being posted to the PR with appropriate warnings.
|
||||||
|
commit = github.repo_from_project(project)._commits.get(A.head_sha)
|
||||||
|
check_runs = commit.check_runs()
|
||||||
|
self.assertEqual(0, len(check_runs))
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
A.head_sha, github.repo_from_project(project)._commits.keys()
|
||||||
|
)
|
||||||
|
check_runs = self.fake_github.getCommitChecks(project, A.head_sha)
|
||||||
|
self.assertEqual(0, len(check_runs))
|
||||||
|
|
||||||
|
expected_warning = (
|
||||||
|
"Failed to update check run tenant-one/checks-api-reporting: "
|
||||||
|
"403 Resource not accessible by integration"
|
||||||
|
)
|
||||||
|
self.assertEqual(2, len(A.comments))
|
||||||
|
self.assertIn(expected_warning, A.comments[0])
|
||||||
|
self.assertIn(expected_warning, A.comments[1])
|
||||||
|
|
|
@ -135,6 +135,37 @@ class TestGithubRequirements(ZuulTestCase):
|
||||||
self.assertEqual(len(self.history), 2)
|
self.assertEqual(len(self.history), 2)
|
||||||
self.assertEqual(self.history[1].name, 'project2-trigger')
|
self.assertEqual(self.history[1].name, 'project2-trigger')
|
||||||
|
|
||||||
|
@simple_layout("layouts/requirements-github.yaml", driver="github")
|
||||||
|
def test_trigger_on_check_run(self):
|
||||||
|
"""Test trigger on: check_run"""
|
||||||
|
project = "org/project15"
|
||||||
|
A = self.fake_github.openFakePullRequest(project, "master", "A")
|
||||||
|
|
||||||
|
# A check_run request with a different name should not cause it to be
|
||||||
|
# enqueued.
|
||||||
|
self.fake_github.emitEvent(
|
||||||
|
A.getCheckRunRequestedEvent("tenant-one/different-check")
|
||||||
|
)
|
||||||
|
self.waitUntilSettled()
|
||||||
|
self.assertEqual(len(self.history), 0)
|
||||||
|
|
||||||
|
# A check_run request with the correct name, but for a different app
|
||||||
|
# should not cause it to be enqueued.
|
||||||
|
self.fake_github.emitEvent(
|
||||||
|
A.getCheckRunRequestedEvent("tenant-one/check", app="other-ci")
|
||||||
|
)
|
||||||
|
self.waitUntilSettled()
|
||||||
|
self.assertEqual(len(self.history), 0)
|
||||||
|
|
||||||
|
# A check_run request with the correct name for the correct app should
|
||||||
|
# cause it to be enqueued.
|
||||||
|
self.fake_github.emitEvent(
|
||||||
|
A.getCheckRunRequestedEvent("tenant-one/check"))
|
||||||
|
|
||||||
|
self.waitUntilSettled()
|
||||||
|
self.assertEqual(len(self.history), 1)
|
||||||
|
self.assertEqual(self.history[0].name, "project15-check-run")
|
||||||
|
|
||||||
@simple_layout('layouts/requirements-github.yaml', driver='github')
|
@simple_layout('layouts/requirements-github.yaml', driver='github')
|
||||||
def test_pipeline_require_review_username(self):
|
def test_pipeline_require_review_username(self):
|
||||||
"Test pipeline requirement: review username"
|
"Test pipeline requirement: review username"
|
||||||
|
|
|
@ -552,6 +552,58 @@ class GithubEventProcessor(object):
|
||||||
event.status = "%s:%s:%s" % _status_as_tuple(self.body)
|
event.status = "%s:%s:%s" % _status_as_tuple(self.body)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
def _event_check_run(self):
|
||||||
|
"""Handles check_run requests.
|
||||||
|
|
||||||
|
This maps to the "Re-run" action on a check run and the "Re-run failed
|
||||||
|
checks" on a check suite in Github.
|
||||||
|
|
||||||
|
This event should be handled similar to a PR commnent or a push.
|
||||||
|
"""
|
||||||
|
action = self.body.get("action")
|
||||||
|
|
||||||
|
# NOTE (felix): We could also handle "requested" events here, which are
|
||||||
|
# sent by Github whenever a change is pushed. But as we are already
|
||||||
|
# listening to push events, this would result in two trigger events
|
||||||
|
# for the same Github event.
|
||||||
|
if action not in ["rerequested", "completed"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
# The head_sha identifies the commit the check_run is requested for
|
||||||
|
# (similar to Github's status API).
|
||||||
|
check_run = self.body.get("check_run")
|
||||||
|
if not check_run:
|
||||||
|
# This shouldn't happen but in case something went wrong it should
|
||||||
|
# also not cause an exception in the event handling
|
||||||
|
return
|
||||||
|
|
||||||
|
project = self.body.get("repository", {}).get("full_name")
|
||||||
|
head_sha = check_run.get("head_sha")
|
||||||
|
|
||||||
|
# Zuul will only accept Github changes that are part of a PR, thus we
|
||||||
|
# must look up the PR first.
|
||||||
|
pr_body = self.connection.getPullBySha(
|
||||||
|
head_sha, project, self.zuul_event_id)
|
||||||
|
if pr_body is None:
|
||||||
|
self.log.debug(
|
||||||
|
"Could not find appropriate PR for SHA %s. "
|
||||||
|
"Skipping check_run event",
|
||||||
|
head_sha
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build a trigger event for the check_run request
|
||||||
|
event = self._pull_request_to_event(pr_body)
|
||||||
|
event.type = "check_run"
|
||||||
|
# Simplify rerequested action to requested
|
||||||
|
if action == "rerequested":
|
||||||
|
action = "requested"
|
||||||
|
event.action = action
|
||||||
|
|
||||||
|
check_run = "%s:%s:%s" % _check_as_tuple(self.body["check_run"])
|
||||||
|
event.check_run = check_run
|
||||||
|
return event
|
||||||
|
|
||||||
def _issue_to_pull_request(self, body):
|
def _issue_to_pull_request(self, body):
|
||||||
number = body.get('issue').get('number')
|
number = body.get('issue').get('number')
|
||||||
project_name = body.get('repository').get('full_name')
|
project_name = body.get('repository').get('full_name')
|
||||||
|
@ -1628,6 +1680,17 @@ class GithubConnection(BaseConnection):
|
||||||
successful = set([s.context for s in commit.status().statuses
|
successful = set([s.context for s in commit.status().statuses
|
||||||
if s.state == 'success'])
|
if s.state == 'success'])
|
||||||
|
|
||||||
|
if self.app_id:
|
||||||
|
try:
|
||||||
|
# Required contexts can be fulfilled by statuses or check runs.
|
||||||
|
successful.update([cr.name for cr in commit.check_runs()
|
||||||
|
if cr.conclusion == 'success'])
|
||||||
|
except github3.exceptions.GitHubException as exc:
|
||||||
|
self.log.error(
|
||||||
|
"Unable to retrieve check runs for commit %s: %s",
|
||||||
|
commit.sha, str(exc)
|
||||||
|
)
|
||||||
|
|
||||||
# Required contexts must be a subset of the successful contexts as
|
# Required contexts must be a subset of the successful contexts as
|
||||||
# we allow additional successful status contexts we don't care about.
|
# we allow additional successful status contexts we don't care about.
|
||||||
return required_contexts.issubset(successful)
|
return required_contexts.issubset(successful)
|
||||||
|
@ -1723,6 +1786,29 @@ class GithubConnection(BaseConnection):
|
||||||
log.debug("Set commit status to %s for sha %s on %s",
|
log.debug("Set commit status to %s for sha %s on %s",
|
||||||
state, sha, project)
|
state, sha, project)
|
||||||
|
|
||||||
|
def getCommitChecks(self, project_name, sha, zuul_event_id=None):
|
||||||
|
log = get_annotated_logger(self.log, zuul_event_id)
|
||||||
|
if not self.app_id:
|
||||||
|
log.debug(
|
||||||
|
"Not authenticated as Github app. Unable to retrieve commit "
|
||||||
|
"checks for sha %s on %s",
|
||||||
|
sha, project_name
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
github = self.getGithubClient(
|
||||||
|
project_name, zuul_event_id=zuul_event_id
|
||||||
|
)
|
||||||
|
url = github.session.build_url(
|
||||||
|
"repos", project_name, "commits", sha, "check-runs")
|
||||||
|
headers = {'Accept': 'application/vnd.github.antiope-preview+json'}
|
||||||
|
params = {"per_page": 100}
|
||||||
|
resp = github.session.get(url, params=params, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
log.debug("Got commit checks for sha %s on %s", sha, project_name)
|
||||||
|
return resp.json().get("check_runs", [])
|
||||||
|
|
||||||
def reviewPull(self, project, pr_number, sha, review, body,
|
def reviewPull(self, project, pr_number, sha, review, body,
|
||||||
zuul_event_id=None):
|
zuul_event_id=None):
|
||||||
github = self.getGithubClient(project, zuul_event_id=zuul_event_id)
|
github = self.getGithubClient(project, zuul_event_id=zuul_event_id)
|
||||||
|
@ -1748,6 +1834,144 @@ class GithubConnection(BaseConnection):
|
||||||
pull_request.remove_label(label)
|
pull_request.remove_label(label)
|
||||||
log.debug("Removed label %s from %s#%s", label, proj, pr_number)
|
log.debug("Removed label %s from %s#%s", label, proj, pr_number)
|
||||||
|
|
||||||
|
def updateCheck(self, project, pr_number, sha, status, completed, context,
|
||||||
|
details_url, message, zuul_event_id=None):
|
||||||
|
log = get_annotated_logger(self.log, zuul_event_id)
|
||||||
|
github = self.getGithubClient(project, zuul_event_id=zuul_event_id)
|
||||||
|
owner, proj = project.split("/")
|
||||||
|
repository = github.repository(owner, proj)
|
||||||
|
|
||||||
|
# Track a list of failed check run operations to report back to Github
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if not self.app_id:
|
||||||
|
# We don't try to update check runs, if we aren't authenticated as
|
||||||
|
# Github app at all. If we are, we still have to ensure that we
|
||||||
|
# don't crash on missing permissions.
|
||||||
|
log.debug(
|
||||||
|
"Not authenticated as Github app. Unable to create or update "
|
||||||
|
"check run '%s' for sha %s on %s",
|
||||||
|
context, sha, project
|
||||||
|
)
|
||||||
|
|
||||||
|
errors.append(
|
||||||
|
"Unable to create or update check {}. Must be authenticated "
|
||||||
|
"as app integration.".format(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
|
output = {"title": "Summary", "summary": message}
|
||||||
|
|
||||||
|
# Currently, the GithubReporter only supports start and end reporting.
|
||||||
|
# During the build no further update will be reported.
|
||||||
|
if completed:
|
||||||
|
# As the buildset itself does not provide a proper end time, we
|
||||||
|
# use the current time instead. Otherwise, we would have to query
|
||||||
|
# all builds contained in the buildset and search for the latest
|
||||||
|
# build.end_time available.
|
||||||
|
completed_at = datetime.datetime.now(utc).isoformat()
|
||||||
|
|
||||||
|
# When reporting the completion of a check_run, we must set the
|
||||||
|
# conclusion, as the status will always be "completed".
|
||||||
|
conclusion = status
|
||||||
|
|
||||||
|
# Unless something went wrong during the start reporting of this
|
||||||
|
# change (e.g. the check_run creation failed), there should already
|
||||||
|
# be a check_run available. If not we will create one.
|
||||||
|
check_runs = []
|
||||||
|
try:
|
||||||
|
check_runs = [
|
||||||
|
c for c in repository.commit(sha).check_runs()
|
||||||
|
if c.name == context
|
||||||
|
]
|
||||||
|
except github3.exceptions.GitHubException as exc:
|
||||||
|
log.error(
|
||||||
|
"Could not retrieve existing check runs for %s#%s on "
|
||||||
|
"sha %s: %s",
|
||||||
|
project, pr_number, sha, str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_runs:
|
||||||
|
log.debug(
|
||||||
|
"Could not find check run %s for %s#%s on sha %s. "
|
||||||
|
"Creating a new one",
|
||||||
|
context, project, pr_number, sha,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_run = repository.create_check_run(
|
||||||
|
name=context,
|
||||||
|
head_sha=sha,
|
||||||
|
conclusion=conclusion,
|
||||||
|
completed_at=completed_at,
|
||||||
|
output=output,
|
||||||
|
details_url=details_url,
|
||||||
|
)
|
||||||
|
except github3.exceptions.GitHubException as exc:
|
||||||
|
# TODO (felix): Should we retry the check_run creation?
|
||||||
|
log.error(
|
||||||
|
"Failed to create check run %s for %s#%s on sha %s: "
|
||||||
|
"%s",
|
||||||
|
context, project, pr_number, sha, str(exc)
|
||||||
|
)
|
||||||
|
errors.append(
|
||||||
|
"Failed to create check run {}: {}".format(
|
||||||
|
context, str(exc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
check_run = check_runs[0]
|
||||||
|
log.debug(
|
||||||
|
"Updating existing check run %s for %s#%s on sha %s "
|
||||||
|
"with status %s",
|
||||||
|
context, project, pr_number, sha, status,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_run.update(
|
||||||
|
conclusion=conclusion,
|
||||||
|
completed_at=completed_at,
|
||||||
|
output=output,
|
||||||
|
details_url=details_url,
|
||||||
|
)
|
||||||
|
except github3.exceptions.GitHubException as exc:
|
||||||
|
log.error(
|
||||||
|
"Failed to update check run %s for %s#%s on sha %s: "
|
||||||
|
"%s",
|
||||||
|
context, project, pr_number, sha, str(exc),
|
||||||
|
)
|
||||||
|
errors.append(
|
||||||
|
"Failed to update check run {}: {}".format(
|
||||||
|
context, str(exc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Report the start of a check run
|
||||||
|
try:
|
||||||
|
check_run = repository.create_check_run(
|
||||||
|
name=context,
|
||||||
|
head_sha=sha,
|
||||||
|
status=status,
|
||||||
|
output=output,
|
||||||
|
details_url=details_url,
|
||||||
|
)
|
||||||
|
except github3.exceptions.GitHubException as exc:
|
||||||
|
# TODO (felix): Should we retry the check run creation?
|
||||||
|
log.error(
|
||||||
|
"Failed to create check run %s for %s#%s on sha %s: %s",
|
||||||
|
context, project, pr_number, sha, str(exc),
|
||||||
|
)
|
||||||
|
errors.append(
|
||||||
|
"Failed to update check run {}: {}".format(
|
||||||
|
context, str(exc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
def getPushedFileNames(self, event):
|
def getPushedFileNames(self, event):
|
||||||
files = set()
|
files = set()
|
||||||
for c in event.commits:
|
for c in event.commits:
|
||||||
|
@ -1768,8 +1992,7 @@ class GithubConnection(BaseConnection):
|
||||||
# by user, so that we can require/trigger by user too.
|
# by user, so that we can require/trigger by user too.
|
||||||
seen = []
|
seen = []
|
||||||
statuses = []
|
statuses = []
|
||||||
for status in self.getCommitStatuses(
|
for status in self.getCommitStatuses(project.name, sha, event):
|
||||||
project.name, sha, event):
|
|
||||||
stuple = _status_as_tuple(status)
|
stuple = _status_as_tuple(status)
|
||||||
if "%s:%s" % (stuple[0], stuple[1]) not in seen:
|
if "%s:%s" % (stuple[0], stuple[1]) not in seen:
|
||||||
statuses.append("%s:%s:%s" % stuple)
|
statuses.append("%s:%s:%s" % stuple)
|
||||||
|
@ -1898,3 +2121,18 @@ def _status_as_tuple(status):
|
||||||
context = status.get('context')
|
context = status.get('context')
|
||||||
state = status.get('state')
|
state = status.get('state')
|
||||||
return (user, context, state)
|
return (user, context, state)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_as_tuple(check):
|
||||||
|
"""Translate a check into a tuple of app, name, conclusion"""
|
||||||
|
|
||||||
|
# A check_run does not contain any "creator" information like a status, but
|
||||||
|
# only the app for/by which it was created.
|
||||||
|
app = check.get("app")
|
||||||
|
if app:
|
||||||
|
slug = app.get("slug")
|
||||||
|
else:
|
||||||
|
slug = "Unknown"
|
||||||
|
name = check.get("name")
|
||||||
|
conclusion = check.get("conclusion")
|
||||||
|
return (slug, name, conclusion)
|
||||||
|
|
|
@ -56,6 +56,7 @@ class GithubTriggerEvent(TriggerEvent):
|
||||||
self.unlabel = None
|
self.unlabel = None
|
||||||
self.action = None
|
self.action = None
|
||||||
self.delivery = None
|
self.delivery = None
|
||||||
|
self.check_runs = None
|
||||||
|
|
||||||
def isPatchsetCreated(self):
|
def isPatchsetCreated(self):
|
||||||
if self.type == 'pull_request':
|
if self.type == 'pull_request':
|
||||||
|
@ -76,6 +77,8 @@ class GithubTriggerEvent(TriggerEvent):
|
||||||
r.append('%s,%s' % (self.change_number, self.patch_number))
|
r.append('%s,%s' % (self.change_number, self.patch_number))
|
||||||
if self.delivery:
|
if self.delivery:
|
||||||
r.append('delivery: %s' % self.delivery)
|
r.append('delivery: %s' % self.delivery)
|
||||||
|
if self.check_runs:
|
||||||
|
r.append('check_runs: %s' % self.check_runs)
|
||||||
return ' '.join(r)
|
return ' '.join(r)
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,7 +220,7 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
||||||
def __init__(self, trigger, types=[], branches=[], refs=[],
|
def __init__(self, trigger, types=[], branches=[], refs=[],
|
||||||
comments=[], actions=[], labels=[], unlabels=[],
|
comments=[], actions=[], labels=[], unlabels=[],
|
||||||
states=[], statuses=[], required_statuses=[],
|
states=[], statuses=[], required_statuses=[],
|
||||||
ignore_deletes=True):
|
check_runs=[], ignore_deletes=True):
|
||||||
|
|
||||||
EventFilter.__init__(self, trigger)
|
EventFilter.__init__(self, trigger)
|
||||||
|
|
||||||
|
@ -237,6 +240,7 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
||||||
self.states = states
|
self.states = states
|
||||||
self.statuses = statuses
|
self.statuses = statuses
|
||||||
self.required_statuses = required_statuses
|
self.required_statuses = required_statuses
|
||||||
|
self.check_runs = check_runs
|
||||||
self.ignore_deletes = ignore_deletes
|
self.ignore_deletes = ignore_deletes
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -254,6 +258,8 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
||||||
ret += ' comments: %s' % ', '.join(self._comments)
|
ret += ' comments: %s' % ', '.join(self._comments)
|
||||||
if self.actions:
|
if self.actions:
|
||||||
ret += ' actions: %s' % ', '.join(self.actions)
|
ret += ' actions: %s' % ', '.join(self.actions)
|
||||||
|
if self.check_runs:
|
||||||
|
ret += ' check_runs: %s' % ','.join(self.check_runs)
|
||||||
if self.labels:
|
if self.labels:
|
||||||
ret += ' labels: %s' % ', '.join(self.labels)
|
ret += ' labels: %s' % ', '.join(self.labels)
|
||||||
if self.unlabels:
|
if self.unlabels:
|
||||||
|
@ -320,6 +326,17 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
||||||
return FalseWithReason("Actions %s doesn't match %s" % (
|
return FalseWithReason("Actions %s doesn't match %s" % (
|
||||||
self.actions, event.action))
|
self.actions, event.action))
|
||||||
|
|
||||||
|
# check_runs are ORed
|
||||||
|
if self.check_runs:
|
||||||
|
check_run_found = False
|
||||||
|
for check_run in self.check_runs:
|
||||||
|
if re2.fullmatch(check_run, event.check_run):
|
||||||
|
check_run_found = True
|
||||||
|
break
|
||||||
|
if not check_run_found:
|
||||||
|
return FalseWithReason("Check_runs %s doesn't match %s" % (
|
||||||
|
self.check_runs, event.check_run))
|
||||||
|
|
||||||
# labels are ORed
|
# labels are ORed
|
||||||
if self.labels and event.label not in self.labels:
|
if self.labels and event.label not in self.labels:
|
||||||
return FalseWithReason("Labels %s doesn't match %s" % (
|
return FalseWithReason("Labels %s doesn't match %s" % (
|
||||||
|
|
|
@ -42,6 +42,7 @@ class GithubReporter(BaseReporter):
|
||||||
super(GithubReporter, self).__init__(driver, connection, config)
|
super(GithubReporter, self).__init__(driver, connection, config)
|
||||||
self._commit_status = self.config.get('status', None)
|
self._commit_status = self.config.get('status', None)
|
||||||
self._create_comment = self.config.get('comment', True)
|
self._create_comment = self.config.get('comment', True)
|
||||||
|
self._check = self.config.get('check', False)
|
||||||
self._merge = self.config.get('merge', False)
|
self._merge = self.config.get('merge', False)
|
||||||
self._labels = self.config.get('label', [])
|
self._labels = self.config.get('label', [])
|
||||||
if not isinstance(self._labels, list):
|
if not isinstance(self._labels, list):
|
||||||
|
@ -77,12 +78,22 @@ class GithubReporter(BaseReporter):
|
||||||
# Comments, labels, and merges can only be performed on pull requests.
|
# Comments, labels, and merges can only be performed on pull requests.
|
||||||
# If the change is not a pull request (e.g. a push) skip them.
|
# If the change is not a pull request (e.g. a push) skip them.
|
||||||
if hasattr(item.change, 'number'):
|
if hasattr(item.change, 'number'):
|
||||||
if self._create_comment:
|
errors_received = False
|
||||||
self.addPullComment(item)
|
|
||||||
if self._labels or self._unlabels:
|
if self._labels or self._unlabels:
|
||||||
self.setLabels(item)
|
self.setLabels(item)
|
||||||
if self._review:
|
if self._review:
|
||||||
self.addReview(item)
|
self.addReview(item)
|
||||||
|
if self._check:
|
||||||
|
check_errors = self.updateCheck(item)
|
||||||
|
# TODO (felix): We could use this mechanism to also report back
|
||||||
|
# errors from label and review actions
|
||||||
|
if check_errors:
|
||||||
|
item.current_build_set.warning_messages.extend(
|
||||||
|
check_errors
|
||||||
|
)
|
||||||
|
errors_received = True
|
||||||
|
if self._create_comment or errors_received:
|
||||||
|
self.addPullComment(item)
|
||||||
if (self._merge):
|
if (self._merge):
|
||||||
self.mergePull(item)
|
self.mergePull(item)
|
||||||
if not item.change.is_merged:
|
if not item.change.is_merged:
|
||||||
|
@ -194,6 +205,38 @@ class GithubReporter(BaseReporter):
|
||||||
self.connection.unlabelPull(project, pr_number, label,
|
self.connection.unlabelPull(project, pr_number, label,
|
||||||
zuul_event_id=item.event)
|
zuul_event_id=item.event)
|
||||||
|
|
||||||
|
def updateCheck(self, item):
|
||||||
|
log = get_annotated_logger(self.log, item.event)
|
||||||
|
message = self._formatItemReport(item)
|
||||||
|
project = item.change.project.name
|
||||||
|
pr_number = item.change.number
|
||||||
|
sha = item.change.patchset
|
||||||
|
|
||||||
|
# Check if the buildset is finished or not. In case it's finished, we
|
||||||
|
# must provide additional parameters when updating the check_run via
|
||||||
|
# the Github API later on.
|
||||||
|
completed = item.current_build_set.result is not None
|
||||||
|
status = self._check
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Updating check for change %s, params %s, context %s, message: %s",
|
||||||
|
item.change, self.config, self.context, message
|
||||||
|
)
|
||||||
|
|
||||||
|
details_url = item.formatStatusUrl()
|
||||||
|
|
||||||
|
return self.connection.updateCheck(
|
||||||
|
project,
|
||||||
|
pr_number,
|
||||||
|
sha,
|
||||||
|
status,
|
||||||
|
completed,
|
||||||
|
self.context,
|
||||||
|
details_url,
|
||||||
|
message,
|
||||||
|
zuul_event_id=item.event,
|
||||||
|
)
|
||||||
|
|
||||||
def setLabels(self, item):
|
def setLabels(self, item):
|
||||||
log = get_annotated_logger(self.log, item.event)
|
log = get_annotated_logger(self.log, item.event)
|
||||||
project = item.change.project.name
|
project = item.change.project.name
|
||||||
|
@ -244,9 +287,11 @@ class GithubReporter(BaseReporter):
|
||||||
this reporter itself is likely to set before submitting.
|
this reporter itself is likely to set before submitting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# check if we report a status, if not we can return an empty list
|
# check if we report a status or a check, if not we can return an
|
||||||
|
# empty list
|
||||||
status = self.config.get('status')
|
status = self.config.get('status')
|
||||||
if not status:
|
check = self.config.get("check")
|
||||||
|
if not any([status, check]):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# we return a status so return the status we report to github
|
# we return a status so return the status we report to github
|
||||||
|
@ -262,6 +307,7 @@ def getSchema():
|
||||||
'label': scalar_or_list(str),
|
'label': scalar_or_list(str),
|
||||||
'unlabel': scalar_or_list(str),
|
'unlabel': scalar_or_list(str),
|
||||||
'review': v.Any('approve', 'request-changes', 'comment'),
|
'review': v.Any('approve', 'request-changes', 'comment'),
|
||||||
'review-body': str
|
'review-body': str,
|
||||||
|
'check': v.Any("in_progress", "success", "failure"),
|
||||||
})
|
})
|
||||||
return github_reporter
|
return github_reporter
|
||||||
|
|
|
@ -33,6 +33,7 @@ class GithubTrigger(BaseTrigger):
|
||||||
branches=to_list(trigger.get('branch')),
|
branches=to_list(trigger.get('branch')),
|
||||||
refs=to_list(trigger.get('ref')),
|
refs=to_list(trigger.get('ref')),
|
||||||
comments=to_list(trigger.get('comment')),
|
comments=to_list(trigger.get('comment')),
|
||||||
|
check_runs=to_list(trigger.get('check')),
|
||||||
labels=to_list(trigger.get('label')),
|
labels=to_list(trigger.get('label')),
|
||||||
unlabels=to_list(trigger.get('unlabel')),
|
unlabels=to_list(trigger.get('unlabel')),
|
||||||
states=to_list(trigger.get('state')),
|
states=to_list(trigger.get('state')),
|
||||||
|
@ -52,7 +53,8 @@ def getSchema():
|
||||||
v.Required('event'):
|
v.Required('event'):
|
||||||
scalar_or_list(v.Any('pull_request',
|
scalar_or_list(v.Any('pull_request',
|
||||||
'pull_request_review',
|
'pull_request_review',
|
||||||
'push')),
|
'push',
|
||||||
|
'check_run')),
|
||||||
'action': scalar_or_list(str),
|
'action': scalar_or_list(str),
|
||||||
'branch': scalar_or_list(str),
|
'branch': scalar_or_list(str),
|
||||||
'ref': scalar_or_list(str),
|
'ref': scalar_or_list(str),
|
||||||
|
@ -61,7 +63,8 @@ def getSchema():
|
||||||
'unlabel': scalar_or_list(str),
|
'unlabel': scalar_or_list(str),
|
||||||
'state': scalar_or_list(str),
|
'state': scalar_or_list(str),
|
||||||
'require-status': scalar_or_list(str),
|
'require-status': scalar_or_list(str),
|
||||||
'status': scalar_or_list(str)
|
'status': scalar_or_list(str),
|
||||||
|
'check': scalar_or_list(str),
|
||||||
}
|
}
|
||||||
|
|
||||||
return github_trigger
|
return github_trigger
|
||||||
|
|
Loading…
Reference in New Issue