Pagure driver - https://pagure.io/pagure/
This change adds a Pagure driver for Zuul. Pagure is a Github like forge https://pagure.io/pagure/. This driver is usable starting with Pagure 5.3. For history: first Pagure PR gated by Zuul is https://pagure.io/test-zuul/pull-request/22 Change-Id: I1711653355ae26a3fff3bb6de3c6fca7113cdd01
This commit is contained in:
parent
f8523ef9f5
commit
7dc1edb4cd
|
@ -64,6 +64,7 @@ Zuul includes the following drivers:
|
|||
|
||||
drivers/gerrit
|
||||
drivers/github
|
||||
drivers/pagure
|
||||
drivers/git
|
||||
drivers/mqtt
|
||||
drivers/smtp
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
:title: Pagure Driver
|
||||
|
||||
.. _pagure_driver:
|
||||
|
||||
Pagure
|
||||
======
|
||||
|
||||
The Pagure driver supports sources, triggers, and reporters. It can
|
||||
interact with the public Pagure.io service as well as site-local
|
||||
installations of Pagure.
|
||||
|
||||
Configure Pagure
|
||||
----------------
|
||||
|
||||
Pagure's project owner must give project Admin access to the Pagure's user
|
||||
that own the API key defined in the Zuul configuration. The API key
|
||||
must at least have the ``Modify an existing project`` access.
|
||||
|
||||
Furthermore Project owner must set the web hook target url in project settings
|
||||
such as: ``http://<zuul-web>/zuul/api/connection/<conn-name>/payload``
|
||||
|
||||
Connection Configuration
|
||||
------------------------
|
||||
|
||||
The supported options in ``zuul.conf`` connections are:
|
||||
|
||||
.. attr:: <pagure connection>
|
||||
|
||||
.. attr:: driver
|
||||
:required:
|
||||
|
||||
.. value:: pagure
|
||||
|
||||
The connection must set ``driver=pagure`` for Pagure connections.
|
||||
|
||||
.. attr:: api_token
|
||||
|
||||
The user's API token with the ``Modify an existing project`` capability.
|
||||
|
||||
.. attr:: server
|
||||
:default: pagure.io
|
||||
|
||||
Hostname of the Pagure server.
|
||||
|
||||
.. attr:: canonical_hostname
|
||||
|
||||
The canonical hostname associated with the git repos on the
|
||||
Pagure server. Defaults to the value of :attr:`<pagure
|
||||
connection>.server`. This is used to identify projects from
|
||||
this connection by name and in preparing repos on the filesystem
|
||||
for use by jobs. Note that Zuul will still only communicate
|
||||
with the Pagure server identified by **server**; this option is
|
||||
useful if users customarily use a different hostname to clone or
|
||||
pull git repos so that when Zuul places them in the job's
|
||||
working directory, they appear under this directory name.
|
||||
|
||||
.. attr:: baseurl
|
||||
:default: https://{server}
|
||||
|
||||
Path to the Pagure web and API interface.
|
||||
|
||||
.. attr:: cloneurl
|
||||
:default: https://{baseurl}
|
||||
|
||||
Path to the Pagure Git repositories. Used to clone.
|
||||
|
||||
Trigger Configuration
|
||||
---------------------
|
||||
Pagure webhook events can be configured as triggers.
|
||||
|
||||
A connection name with the Pagure driver can take multiple events with
|
||||
the following options.
|
||||
|
||||
.. attr:: pipeline.trigger.<pagure source>
|
||||
|
||||
The dictionary passed to the Pagure pipeline ``trigger`` attribute
|
||||
supports the following attributes:
|
||||
|
||||
.. attr:: event
|
||||
:required:
|
||||
|
||||
The event from Pagure. Supported events are:
|
||||
|
||||
.. value:: pg_pull_request
|
||||
|
||||
.. value:: pg_pull_request_review
|
||||
|
||||
.. value:: pg_push
|
||||
|
||||
.. attr:: action
|
||||
|
||||
A :value:`pipeline.trigger.<pagure source>.event.pg_pull_request`
|
||||
event will have associated action(s) to trigger from. The
|
||||
supported actions are:
|
||||
|
||||
.. value:: opened
|
||||
|
||||
Pull request opened.
|
||||
|
||||
.. value:: changed
|
||||
|
||||
Pull request synchronized.
|
||||
|
||||
.. value:: comment
|
||||
|
||||
Comment added to pull request.
|
||||
|
||||
.. value:: status
|
||||
|
||||
Status set on pull request.
|
||||
|
||||
A :value:`pipeline.trigger.<pagure
|
||||
source>.event.pg_pull_request_review` event will have associated
|
||||
action(s) to trigger from. The supported actions are:
|
||||
|
||||
.. value:: thumbsup
|
||||
|
||||
Positive pull request review added.
|
||||
|
||||
.. value:: thumbsdown
|
||||
|
||||
Negative pull request review added.
|
||||
|
||||
.. attr:: comment
|
||||
|
||||
This is only used for ``pg_pull_request`` ``comment`` actions. It
|
||||
accepts a list of regexes that are searched for in the comment
|
||||
string. If any of these regexes matches a portion of the comment
|
||||
string the trigger is matched. ``comment: retrigger`` will
|
||||
match when comments containing 'retrigger' somewhere in the
|
||||
comment text are added to a pull request.
|
||||
|
||||
.. attr:: status
|
||||
|
||||
This is used for ``pg_pull_request`` and ``status`` actions. It
|
||||
accepts a list of strings each of which matches the user setting
|
||||
the status, the status context, and the status itself in the
|
||||
format of ``status``. For example, ``success`` or ``failure``.
|
||||
|
||||
.. attr:: ref
|
||||
|
||||
This is only used for ``pg_push`` events. This field is treated as
|
||||
a regular expression and multiple refs may be listed. Pagure
|
||||
always sends full ref name, eg. ``refs/tags/bar`` and this
|
||||
string is matched against the regular expression.
|
||||
|
||||
Reporter Configuration
|
||||
----------------------
|
||||
Zuul reports back to Pagure via Pagure API. Available reports include a PR
|
||||
comment containing the build results, a commit status on start, success and
|
||||
failure, and a merge of the PR itself. Status name, description, and context
|
||||
is taken from the pipeline.
|
||||
|
||||
.. attr:: pipeline.<reporter>.<pagure source>
|
||||
|
||||
To report to Pagure, the dictionaries passed to any of the pipeline
|
||||
:ref:`reporter<reporters>` attributes support the following
|
||||
attributes:
|
||||
|
||||
.. attr:: status
|
||||
|
||||
String value (``pending``, ``success``, ``failure``) that the
|
||||
reporter should set as the commit status on Pagure.
|
||||
|
||||
.. attr:: status-url
|
||||
:default: web.status_url or the empty string
|
||||
|
||||
String value for a link url to set in the Pagure status. Defaults to the
|
||||
zuul server status_url, or the empty string if that is unset.
|
||||
|
||||
.. attr:: comment
|
||||
:default: true
|
||||
|
||||
Boolean value that determines if the reporter should add a
|
||||
comment to the pipeline status to the Pagure Pull Request. Only
|
||||
used for Pull Request based items.
|
||||
|
||||
.. attr:: merge
|
||||
:default: false
|
||||
|
||||
Boolean value that determines if the reporter should merge the
|
||||
pull Request. Only used for Pull Request based items.
|
||||
|
||||
|
||||
Requirements Configuration
|
||||
--------------------------
|
||||
|
||||
As described in :attr:`pipeline.require` pipelines may specify that items meet
|
||||
certain conditions in order to be enqueued into the pipeline. These conditions
|
||||
vary according to the source of the project in question. To supply
|
||||
requirements for changes from a Pagure source named ``pagure``, create a
|
||||
configuration such as the following::
|
||||
|
||||
pipeline:
|
||||
require:
|
||||
pagure:
|
||||
score: 1
|
||||
merged: false
|
||||
status: success
|
||||
|
||||
This indicates that changes originating from the Pagure connection
|
||||
must have a score of *1*, a CI status *success* and not being already merged.
|
||||
|
||||
.. attr:: pipeline.require.<pagure source>
|
||||
|
||||
The dictionary passed to the Pagure pipeline `require` attribute
|
||||
supports the following attributes:
|
||||
|
||||
.. attr:: score
|
||||
|
||||
If present, the minimal score a Pull Request must reached.
|
||||
|
||||
.. attr:: status
|
||||
|
||||
If present, the CI status a Pull Request must have.
|
||||
|
||||
.. attr:: merged
|
||||
|
||||
A boolean value (``true`` or ``false``) that indicates whether
|
||||
the Pull Request must be merged or not in order to be enqueued.
|
||||
|
||||
.. attr:: open
|
||||
|
||||
A boolean value (``true`` or ``false``) that indicates whether
|
||||
the Pull Request must be open or closed in order to be enqueued.
|
||||
|
||||
Reference pipelines configuration
|
||||
---------------------------------
|
||||
|
||||
Here is an example of standard pipelines you may want to define::
|
||||
|
||||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
require:
|
||||
pagure.io:
|
||||
merged: False
|
||||
trigger:
|
||||
pagure.io:
|
||||
- event: pg_pull_request
|
||||
action: comment
|
||||
comment: (?i)^\s*recheck\s*$
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- opened
|
||||
- changed
|
||||
start:
|
||||
pagure.io:
|
||||
status: 'pending'
|
||||
comment: false
|
||||
sqlreporter:
|
||||
success:
|
||||
pagure.io:
|
||||
status: 'success'
|
||||
sqlreporter:
|
||||
failure:
|
||||
pagure.io:
|
||||
status: 'failure'
|
||||
sqlreporter:
|
||||
|
||||
- pipeline:
|
||||
name: gate
|
||||
manager: dependent
|
||||
precedence: high
|
||||
require:
|
||||
pagure.io:
|
||||
score: 1
|
||||
merged: False
|
||||
status: success
|
||||
sqlreporter:
|
||||
trigger:
|
||||
pagure.io:
|
||||
- event: pg_pull_request
|
||||
action: status
|
||||
status: success
|
||||
- event: pg_pull_request_review
|
||||
action: thumbsup
|
||||
start:
|
||||
pagure.io:
|
||||
status: 'pending'
|
||||
comment: false
|
||||
sqlreporter:
|
||||
success:
|
||||
pagure.io:
|
||||
status: 'success'
|
||||
merge: true
|
||||
comment: true
|
||||
sqlreporter:
|
||||
failure:
|
||||
pagure.io:
|
||||
status: 'failure'
|
||||
comment: true
|
||||
sqlreporter:
|
||||
|
||||
- pipeline:
|
||||
name: post
|
||||
post-review: true
|
||||
manager: independent
|
||||
precedence: low
|
||||
trigger:
|
||||
pagure.io:
|
||||
- event: pg_push
|
||||
ref: ^refs/heads/.*$
|
||||
success:
|
||||
sqlreporter:
|
349
tests/base.py
349
tests/base.py
|
@ -61,6 +61,7 @@ import tests.fakegithub
|
|||
import zuul.driver.gerrit.gerritsource as gerritsource
|
||||
import zuul.driver.gerrit.gerritconnection as gerritconnection
|
||||
import zuul.driver.github.githubconnection as githubconnection
|
||||
import zuul.driver.pagure.pagureconnection as pagureconnection
|
||||
import zuul.driver.github
|
||||
import zuul.driver.sql
|
||||
import zuul.scheduler
|
||||
|
@ -800,6 +801,331 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
|
|||
return 'file://' + os.path.join(self.upstream_root, project.name)
|
||||
|
||||
|
||||
class PagureChangeReference(git.Reference):
|
||||
_common_path_default = "refs/pull"
|
||||
_points_to_commits_only = True
|
||||
|
||||
|
||||
class FakePagurePullRequest(object):
|
||||
log = logging.getLogger("zuul.test.FakePagurePullRequest")
|
||||
|
||||
def __init__(self, pagure, number, project, branch,
|
||||
subject, upstream_root, files=[], number_of_commits=1,
|
||||
initial_comment=None):
|
||||
self.pagure = pagure
|
||||
self.source = pagure
|
||||
self.number = number
|
||||
self.project = project
|
||||
self.branch = branch
|
||||
self.subject = subject
|
||||
self.upstream_root = upstream_root
|
||||
self.number_of_commits = 0
|
||||
self.status = 'Open'
|
||||
self.initial_comment = initial_comment
|
||||
self.uuid = uuid.uuid4().hex
|
||||
self.comments = []
|
||||
self.flags = []
|
||||
self.files = {}
|
||||
self.cached_merge_status = ''
|
||||
self.threshold_reached = False
|
||||
self.commit_stop = None
|
||||
self.commit_start = None
|
||||
self.threshold_reached = False
|
||||
self.upstream_root = upstream_root
|
||||
self.cached_merge_status = 'MERGE'
|
||||
self.url = "https://%s/%s/pull-request/%s" % (
|
||||
self.pagure.server, self.project, self.number)
|
||||
self.is_merged = False
|
||||
self.pr_ref = self._createPRRef()
|
||||
self._addCommitInPR(files=files)
|
||||
self._updateTimeStamp()
|
||||
|
||||
def _getPullRequestEvent(self, action):
|
||||
name = 'pg_pull_request'
|
||||
data = {
|
||||
'msg': {
|
||||
'pullrequest': {
|
||||
'branch': self.branch,
|
||||
'comments': self.comments,
|
||||
'commit_start': self.commit_start,
|
||||
'commit_stop': self.commit_stop,
|
||||
'date_created': '0',
|
||||
'id': self.number,
|
||||
'project': {
|
||||
'fullname': self.project,
|
||||
},
|
||||
'status': self.status,
|
||||
'subject': self.subject,
|
||||
'uid': self.uuid,
|
||||
}
|
||||
},
|
||||
'msg_id': str(uuid.uuid4()),
|
||||
'timestamp': 1427459070,
|
||||
'topic': action
|
||||
}
|
||||
if action == 'pull-request.flag.added':
|
||||
data['msg']['flag'] = self.flags[0]
|
||||
return (name, data)
|
||||
|
||||
def getPullRequestOpenedEvent(self):
|
||||
return self._getPullRequestEvent('pull-request.new')
|
||||
|
||||
def getPullRequestUpdatedEvent(self):
|
||||
self._addCommitInPR()
|
||||
self.addComment(
|
||||
"**1 new commit added**\n\n * ``Bump``\n",
|
||||
True)
|
||||
return self._getPullRequestEvent('pull-request.comment.added')
|
||||
|
||||
def getPullRequestCommentedEvent(self, message):
|
||||
self.addComment(message)
|
||||
return self._getPullRequestEvent('pull-request.comment.added')
|
||||
|
||||
def getPullRequestStatusSetEvent(self, status):
|
||||
self.addFlag(
|
||||
status, "https://url", "Build %s" % status)
|
||||
return self._getPullRequestEvent('pull-request.flag.added')
|
||||
|
||||
def addFlag(self, status, url, comment, username="Pingou"):
|
||||
flag = {
|
||||
"username": username,
|
||||
"comment": comment,
|
||||
"status": status,
|
||||
"url": url
|
||||
}
|
||||
self.flags.insert(0, flag)
|
||||
self._updateTimeStamp()
|
||||
|
||||
def editInitialComment(self, initial_comment):
|
||||
self.initial_comment = initial_comment
|
||||
self._updateTimeStamp()
|
||||
|
||||
def addComment(self, message, notification=False, fullname=None):
|
||||
self.comments.append({
|
||||
'comment': message,
|
||||
'notification': notification,
|
||||
'date_created': str(int(time.time())),
|
||||
'user': {
|
||||
'fullname': fullname or 'Pingou'
|
||||
}}
|
||||
)
|
||||
self._updateTimeStamp()
|
||||
|
||||
def getPRReference(self):
|
||||
return '%s/head' % self.number
|
||||
|
||||
def _getRepo(self):
|
||||
repo_path = os.path.join(self.upstream_root, self.project)
|
||||
return git.Repo(repo_path)
|
||||
|
||||
def _createPRRef(self):
|
||||
repo = self._getRepo()
|
||||
return PagureChangeReference.create(
|
||||
repo, self.getPRReference(), 'refs/tags/init')
|
||||
|
||||
def addCommit(self, files=[]):
|
||||
"""Adds a commit on top of the actual PR head."""
|
||||
self._addCommitInPR(files=files)
|
||||
self._updateTimeStamp()
|
||||
|
||||
def forcePush(self, files=[]):
|
||||
"""Clears actual commits and add a commit on top of the base."""
|
||||
self._addCommitInPR(files=files, reset=True)
|
||||
self._updateTimeStamp()
|
||||
|
||||
def _addCommitInPR(self, files=[], reset=False):
|
||||
repo = self._getRepo()
|
||||
ref = repo.references[self.getPRReference()]
|
||||
if reset:
|
||||
self.number_of_commits = 0
|
||||
ref.set_object('refs/tags/init')
|
||||
self.number_of_commits += 1
|
||||
repo.head.reference = ref
|
||||
repo.git.clean('-x', '-f', '-d')
|
||||
|
||||
if files:
|
||||
self.files = files
|
||||
else:
|
||||
fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
|
||||
self.files = {fn: "test %s %s\n" % (self.branch, self.number)}
|
||||
msg = self.subject + '-' + str(self.number_of_commits)
|
||||
for fn, content in self.files.items():
|
||||
fn = os.path.join(repo.working_dir, fn)
|
||||
with open(fn, 'w') as f:
|
||||
f.write(content)
|
||||
repo.index.add([fn])
|
||||
|
||||
self.commit_stop = repo.index.commit(msg).hexsha
|
||||
if not self.commit_start:
|
||||
self.commit_start = self.commit_stop
|
||||
|
||||
repo.create_head(self.getPRReference(), self.commit_stop, force=True)
|
||||
self.pr_ref.set_commit(self.commit_stop)
|
||||
repo.head.reference = 'master'
|
||||
repo.git.clean('-x', '-f', '-d')
|
||||
repo.heads['master'].checkout()
|
||||
|
||||
def _updateTimeStamp(self):
|
||||
self.last_updated = str(int(time.time()))
|
||||
|
||||
|
||||
class FakePagureAPIClient(pagureconnection.PagureAPIClient):
|
||||
log = logging.getLogger("zuul.test.FakePagureAPIClient")
|
||||
|
||||
def __init__(self, baseurl, api_token, project,
|
||||
token_exp_date=None, pull_requests_db={}):
|
||||
super(FakePagureAPIClient, self).__init__(
|
||||
baseurl, api_token, project, token_exp_date)
|
||||
self.session = None
|
||||
self.pull_requests = pull_requests_db
|
||||
|
||||
def gen_error(self):
|
||||
return {
|
||||
'error': 'some error',
|
||||
'error_code': 'some error code'
|
||||
}
|
||||
|
||||
def _get_pr(self, match):
|
||||
project, number = match.groups()
|
||||
pr = self.pull_requests.get(project, {}).get(number)
|
||||
if not pr:
|
||||
return self.gen_error()
|
||||
return pr
|
||||
|
||||
def get(self, url):
|
||||
self.log.debug("Getting resource %s ..." % url)
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
return {
|
||||
'branch': pr.branch,
|
||||
'subject': pr.subject,
|
||||
'status': pr.status,
|
||||
'initial_comment': pr.initial_comment,
|
||||
'last_updated': pr.last_updated,
|
||||
'comments': pr.comments,
|
||||
'commit_stop': pr.commit_stop,
|
||||
'threshold_reached': pr.threshold_reached,
|
||||
'cached_merge_status': pr.cached_merge_status
|
||||
}
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/flag$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
return {'flags': pr.flags}
|
||||
|
||||
match = re.match('.+/api/0/(.+)/git/branches$', url)
|
||||
if match:
|
||||
# project = match.groups()[0]
|
||||
return {'branches': ['master']}
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/diffstats$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
return pr.files
|
||||
|
||||
def post(self, url, params=None):
|
||||
|
||||
self.log.info(
|
||||
"Posting on resource %s, params (%s) ..." % (url, params))
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/merge$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
pr.status = 'Merged'
|
||||
pr.is_merged = True
|
||||
|
||||
if not params:
|
||||
return self.gen_error()
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/flag$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
pr.flags.insert(0, params)
|
||||
|
||||
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/comment$', url)
|
||||
if match:
|
||||
pr = self._get_pr(match)
|
||||
pr.addComment(params['comment'])
|
||||
|
||||
|
||||
class FakePagureConnection(pagureconnection.PagureConnection):
|
||||
log = logging.getLogger("zuul.test.FakePagureConnection")
|
||||
|
||||
def __init__(self, driver, connection_name, connection_config, rpcclient,
|
||||
changes_db=None, upstream_root=None):
|
||||
super(FakePagureConnection, self).__init__(driver, connection_name,
|
||||
connection_config)
|
||||
self.connection_name = connection_name
|
||||
self.pr_number = 0
|
||||
self.pull_requests = changes_db
|
||||
self.statuses = {}
|
||||
self.upstream_root = upstream_root
|
||||
self.reports = []
|
||||
self.rpcclient = rpcclient
|
||||
self.cloneurl = self.upstream_root
|
||||
|
||||
def _refresh_project_connectors(self, project):
|
||||
connector = self.connectors.setdefault(
|
||||
project, {'api_client': None, 'webhook_token': None})
|
||||
api_token_exp_date = int(time.time()) + 60 * 24 * 3600
|
||||
connector['api_client'] = FakePagureAPIClient(
|
||||
self.baseurl, "fake_api_token-%s" % project, project,
|
||||
token_exp_date=api_token_exp_date,
|
||||
pull_requests_db=self.pull_requests)
|
||||
connector['webhook_token'] = "fake_webhook_token-%s" % project
|
||||
return connector
|
||||
|
||||
def emitEvent(self, event, use_zuulweb=False, project=None):
|
||||
name, payload = event
|
||||
secret = 'fake_webhook_token-%s' % project
|
||||
if use_zuulweb:
|
||||
payload = json.dumps(payload).encode('utf-8')
|
||||
signature, _ = pagureconnection._sign_request(payload, secret)
|
||||
headers = {'x-pagure-signature': signature,
|
||||
'x-pagure-project': project}
|
||||
return requests.post(
|
||||
'http://127.0.0.1:%s/api/connection/%s/payload'
|
||||
% (self.zuul_web_port, self.connection_name),
|
||||
data=payload, headers=headers)
|
||||
else:
|
||||
job = self.rpcclient.submitJob(
|
||||
'pagure:%s:payload' % self.connection_name,
|
||||
{'payload': payload})
|
||||
return json.loads(job.data[0])
|
||||
|
||||
def openFakePullRequest(self, project, branch, subject, files=[],
|
||||
initial_comment=None):
|
||||
self.pr_number += 1
|
||||
pull_request = FakePagurePullRequest(
|
||||
self, self.pr_number, project, branch, subject, self.upstream_root,
|
||||
files=files, initial_comment=initial_comment)
|
||||
self.pull_requests.setdefault(
|
||||
project, {})[str(self.pr_number)] = pull_request
|
||||
return pull_request
|
||||
|
||||
def getGitReceiveEvent(self, project):
|
||||
name = 'pg_push'
|
||||
repo_path = os.path.join(self.upstream_root, project)
|
||||
repo = git.Repo(repo_path)
|
||||
headsha = repo.head.commit.hexsha
|
||||
data = {
|
||||
'msg': {
|
||||
'project_fullname': project,
|
||||
'branch': 'master',
|
||||
'stop_commit': headsha,
|
||||
},
|
||||
'msg_id': str(uuid.uuid4()),
|
||||
'timestamp': 1427459070,
|
||||
'topic': 'git.receive',
|
||||
}
|
||||
return (name, data)
|
||||
|
||||
def setZuulWebPort(self, port):
|
||||
self.zuul_web_port = port
|
||||
|
||||
|
||||
class GithubChangeReference(git.Reference):
|
||||
_common_path_default = "refs/pull"
|
||||
_points_to_commits_only = True
|
||||
|
@ -1378,7 +1704,7 @@ class FakeBuild(object):
|
|||
self.changes = None
|
||||
items = self.parameters['zuul']['items']
|
||||
self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
|
||||
for x in items if 'change' in x])
|
||||
for x in items if 'change' in x])
|
||||
if 'change' in items[-1]:
|
||||
self.change = ' '.join((items[-1]['change'],
|
||||
items[-1]['patchset']))
|
||||
|
@ -2184,7 +2510,8 @@ class ZuulWebFixture(fixtures.Fixture):
|
|||
self.connections.configure(
|
||||
config,
|
||||
include_drivers=[zuul.driver.sql.SQLDriver,
|
||||
zuul.driver.github.GithubDriver])
|
||||
zuul.driver.github.GithubDriver,
|
||||
zuul.driver.pagure.PagureDriver])
|
||||
if info is None:
|
||||
self.info = zuul.model.WebInfo()
|
||||
else:
|
||||
|
@ -2635,6 +2962,7 @@ class ZuulTestCase(BaseTestCase):
|
|||
# a virtual canonical database given by the configured hostname
|
||||
self.gerrit_changes_dbs = {}
|
||||
self.github_changes_dbs = {}
|
||||
self.pagure_changes_dbs = {}
|
||||
|
||||
def getGerritConnection(driver, name, config):
|
||||
db = self.gerrit_changes_dbs.setdefault(config['server'], {})
|
||||
|
@ -2692,6 +3020,22 @@ class ZuulTestCase(BaseTestCase):
|
|||
'zuul.driver.github.GithubDriver.getConnection',
|
||||
getGithubConnection))
|
||||
|
||||
def getPagureConnection(driver, name, config):
|
||||
server = config.get('server', 'pagure.io')
|
||||
db = self.pagure_changes_dbs.setdefault(server, {})
|
||||
con = FakePagureConnection(
|
||||
driver, name, config,
|
||||
self.rpcclient,
|
||||
changes_db=db,
|
||||
upstream_root=self.upstream_root)
|
||||
self.event_queues.append(con.event_queue)
|
||||
setattr(self, 'fake_' + name, con)
|
||||
return con
|
||||
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'zuul.driver.pagure.PagureDriver.getConnection',
|
||||
getPagureConnection))
|
||||
|
||||
# Set up smtp related fakes
|
||||
# TODO(jhesketh): This should come from lib.connections for better
|
||||
# coverage
|
||||
|
@ -3623,7 +3967,6 @@ class ZuulTestCase(BaseTestCase):
|
|||
self.addCleanup(_restoreTenantConfig)
|
||||
|
||||
def addEvent(self, connection, event):
|
||||
|
||||
"""Inject a Fake (Gerrit) event.
|
||||
|
||||
This method accepts a JSON-encoded event and simulates Zuul
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
- tenant:
|
||||
name: tenant-one
|
||||
source:
|
||||
gerrit:
|
||||
config-projects:
|
||||
- common-config-gerrit
|
||||
untrusted-projects:
|
||||
- gerrit/project1
|
||||
pagure:
|
||||
untrusted-projects:
|
||||
- pagure/project2
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
137
tests/fixtures/config/cross-source-pagure/git/common-config-gerrit/zuul.yaml
vendored
Normal file
137
tests/fixtures/config/cross-source-pagure/git/common-config-gerrit/zuul.yaml
vendored
Normal file
|
@ -0,0 +1,137 @@
|
|||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: patchset-created
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- changed
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 1
|
||||
pagure: {}
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -1
|
||||
pagure: {}
|
||||
|
||||
- pipeline:
|
||||
name: gate
|
||||
manager: dependent
|
||||
success-message: Build succeeded (gate).
|
||||
require:
|
||||
pagure:
|
||||
score: 1
|
||||
merged: False
|
||||
status: success
|
||||
gerrit:
|
||||
approval:
|
||||
- Approved: 1
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: comment-added
|
||||
approval:
|
||||
- Approved: 1
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: status
|
||||
status: success
|
||||
- event: pg_pull_request_review
|
||||
action: thumbsup
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 2
|
||||
submit: true
|
||||
pagure:
|
||||
merge: true
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -2
|
||||
pagure: {}
|
||||
start:
|
||||
gerrit:
|
||||
Verified: 0
|
||||
pagure: {}
|
||||
precedence: high
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
|
||||
- job:
|
||||
name: project-merge
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-merge.yaml
|
||||
|
||||
- job:
|
||||
name: project-test1
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-test1.yaml
|
||||
|
||||
- job:
|
||||
name: project-test2
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-test2.yaml
|
||||
|
||||
- job:
|
||||
name: project1-project2-integration
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project1-project2-integration.yaml
|
||||
|
||||
- project:
|
||||
name: gerrit/project1
|
||||
check:
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
gate:
|
||||
queue: integrated
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
|
||||
- project:
|
||||
name: pagure/project2
|
||||
check:
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
gate:
|
||||
queue: integrated
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
135
tests/fixtures/config/cross-source-pagure/git/github_common-config/zuul.yaml
vendored
Normal file
135
tests/fixtures/config/cross-source-pagure/git/github_common-config/zuul.yaml
vendored
Normal file
|
@ -0,0 +1,135 @@
|
|||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
trigger:
|
||||
github:
|
||||
- event: pull_request
|
||||
action:
|
||||
- edited
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- changed
|
||||
success:
|
||||
github: {}
|
||||
pagure: {}
|
||||
failure:
|
||||
github: {}
|
||||
pagure: {}
|
||||
|
||||
- pipeline:
|
||||
name: gate
|
||||
manager: dependent
|
||||
success-message: Build succeeded (gate).
|
||||
require:
|
||||
pagure:
|
||||
score: 1
|
||||
merged: False
|
||||
status: success
|
||||
github:
|
||||
label: approved
|
||||
trigger:
|
||||
github:
|
||||
- event: pull_request
|
||||
action: edited
|
||||
- event: pull_request
|
||||
action: labeled
|
||||
label: approved
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: status
|
||||
status: success
|
||||
- event: pg_pull_request_review
|
||||
action: thumbsup
|
||||
success:
|
||||
github:
|
||||
merge: true
|
||||
pagure:
|
||||
merge: true
|
||||
failure:
|
||||
github: {}
|
||||
pagure: {}
|
||||
start:
|
||||
github: {}
|
||||
pagure: {}
|
||||
precedence: high
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
|
||||
- job:
|
||||
name: project-merge
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-merge.yaml
|
||||
|
||||
- job:
|
||||
name: project-test1
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-test1.yaml
|
||||
|
||||
- job:
|
||||
name: project-test2
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project-test2.yaml
|
||||
|
||||
- job:
|
||||
name: project1-project2-integration
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: label1
|
||||
run: playbooks/project1-project2-integration.yaml
|
||||
|
||||
- project:
|
||||
name: github/project1
|
||||
check:
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
gate:
|
||||
queue: integrated
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
|
||||
- project:
|
||||
name: pagure/project2
|
||||
check:
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
||||
gate:
|
||||
queue: integrated
|
||||
jobs:
|
||||
- project-merge
|
||||
- project-test1:
|
||||
dependencies: project-merge
|
||||
- project-test2:
|
||||
dependencies: project-merge
|
||||
- project1-project2-integration:
|
||||
dependencies: project-merge
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1,11 @@
|
|||
- tenant:
|
||||
name: tenant-one
|
||||
source:
|
||||
github:
|
||||
config-projects:
|
||||
- github/common-config
|
||||
untrusted-projects:
|
||||
- github/project1
|
||||
pagure:
|
||||
untrusted-projects:
|
||||
- pagure/project2
|
|
@ -0,0 +1,60 @@
|
|||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: comment
|
||||
comment: (?i)^\s*recheck\s*$
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- opened
|
||||
- changed
|
||||
start:
|
||||
pagure:
|
||||
status: 'pending'
|
||||
comment: True
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
comment: True
|
||||
failure:
|
||||
pagure:
|
||||
status: 'failure'
|
||||
comment: True
|
||||
|
||||
- pipeline:
|
||||
name: post
|
||||
post-review: true
|
||||
manager: independent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_push
|
||||
ref: ^refs/heads/.*$
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
|
||||
- job:
|
||||
name: project-test1
|
||||
run: playbooks/project-test1.yaml
|
||||
|
||||
- job:
|
||||
name: project-test2
|
||||
run: playbooks/project-test2.yaml
|
||||
|
||||
- job:
|
||||
name: project-post-job
|
||||
run: playbooks/project-post.yaml
|
||||
|
||||
- project:
|
||||
name: org/project
|
||||
check:
|
||||
jobs:
|
||||
- project-test1
|
||||
- project-test2
|
||||
post:
|
||||
jobs:
|
||||
- project-post-job
|
|
@ -0,0 +1,65 @@
|
|||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: comment
|
||||
|
||||
- pipeline:
|
||||
name: gate
|
||||
manager: dependent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: comment
|
||||
success:
|
||||
pagure:
|
||||
merge: true
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
|
||||
- job:
|
||||
name: project1-test
|
||||
run: playbooks/project1-test.yaml
|
||||
|
||||
- job:
|
||||
name: project2-test
|
||||
run: playbooks/project2-test.yaml
|
||||
|
||||
- job:
|
||||
name: project3-test
|
||||
run: playbooks/project3-test.yaml
|
||||
|
||||
- job:
|
||||
name: project4-test
|
||||
run: playbooks/project4-test.yaml
|
||||
|
||||
- project:
|
||||
name: org/project1
|
||||
check:
|
||||
jobs:
|
||||
- project1-test
|
||||
|
||||
- project:
|
||||
name: org/project2
|
||||
check:
|
||||
jobs:
|
||||
- project2-test
|
||||
|
||||
- project:
|
||||
name: org/project3
|
||||
gate:
|
||||
queue: cogated
|
||||
jobs:
|
||||
- project3-test
|
||||
|
||||
- project:
|
||||
name: org/project4
|
||||
gate:
|
||||
queue: cogated
|
||||
jobs:
|
||||
- project4-test
|
|
@ -0,0 +1,46 @@
|
|||
- pipeline:
|
||||
name: check-merge
|
||||
manager: independent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- opened
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
merge: true
|
||||
|
||||
- pipeline:
|
||||
name: gate-merge
|
||||
manager: dependent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action:
|
||||
- opened
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
merge: true
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
|
||||
- job:
|
||||
name: project-test
|
||||
run: playbooks/project-test.yaml
|
||||
|
||||
- project:
|
||||
name: org/project1
|
||||
check-merge:
|
||||
jobs:
|
||||
- project-test
|
||||
|
||||
- project:
|
||||
name: org/project2
|
||||
gate-merge:
|
||||
jobs:
|
||||
- project-test
|
|
@ -0,0 +1,66 @@
|
|||
- pipeline:
|
||||
name: req-score-1
|
||||
manager: independent
|
||||
require:
|
||||
pagure:
|
||||
score: 1
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request_review
|
||||
action: thumbsup
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
|
||||
- pipeline:
|
||||
name: req-score-2
|
||||
manager: independent
|
||||
require:
|
||||
pagure:
|
||||
score: 2
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request_review
|
||||
action: thumbsup
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
|
||||
- pipeline:
|
||||
name: trigger-flag
|
||||
manager: independent
|
||||
trigger:
|
||||
pagure:
|
||||
- event: pg_pull_request
|
||||
action: status
|
||||
status: success
|
||||
success:
|
||||
pagure:
|
||||
status: 'success'
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
|
||||
- job:
|
||||
name: project-test
|
||||
run: playbooks/project-test.yaml
|
||||
|
||||
- project:
|
||||
name: org/project1
|
||||
req-score-1:
|
||||
jobs:
|
||||
- project-test
|
||||
|
||||
- project:
|
||||
name: org/project2
|
||||
req-score-2:
|
||||
jobs:
|
||||
- project-test
|
||||
|
||||
- project:
|
||||
name: org/project3
|
||||
trigger-flag:
|
||||
jobs:
|
||||
- project-test
|
|
@ -0,0 +1,39 @@
|
|||
[gearman]
|
||||
server=127.0.0.1
|
||||
|
||||
[statsd]
|
||||
# note, use 127.0.0.1 rather than localhost to avoid getting ipv6
|
||||
# see: https://github.com/jsocol/pystatsd/issues/61
|
||||
server=127.0.0.1
|
||||
|
||||
[scheduler]
|
||||
tenant_config=main.yaml
|
||||
|
||||
[merger]
|
||||
git_dir=/tmp/zuul-test/merger-git
|
||||
git_user_email=zuul@example.com
|
||||
git_user_name=zuul
|
||||
|
||||
[executor]
|
||||
git_dir=/tmp/zuul-test/executor-git
|
||||
|
||||
[connection gerrit]
|
||||
driver=gerrit
|
||||
server=review.example.com
|
||||
user=jenkins
|
||||
sshkey=fake_id_rsa_path
|
||||
|
||||
[connection github]
|
||||
driver=github
|
||||
webhook_token=0000000000000000000000000000000000000000
|
||||
|
||||
[connection pagure]
|
||||
driver=pagure
|
||||
api_token=0000000000000000000000000000000000000000
|
||||
|
||||
[connection smtp]
|
||||
driver=smtp
|
||||
server=localhost
|
||||
port=25
|
||||
default_from=zuul@example.com
|
||||
default_to=you@example.com
|
|
@ -0,0 +1,18 @@
|
|||
[gearman]
|
||||
server=127.0.0.1
|
||||
|
||||
[web]
|
||||
status_url=http://zuul.example.com/status/#{change.number},{change.patchset}
|
||||
|
||||
[merger]
|
||||
git_dir=/tmp/zuul-test/git
|
||||
git_user_email=zuul@example.com
|
||||
git_user_name=zuul
|
||||
|
||||
[executor]
|
||||
git_dir=/tmp/zuul-test/executor-git
|
||||
|
||||
[connection pagure]
|
||||
driver=pagure
|
||||
server=pagure
|
||||
api_token=0000000000000000000000000000000000000000
|
|
@ -0,0 +1,872 @@
|
|||
# Copyright 2019 Red Hat
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
import yaml
|
||||
import time
|
||||
import socket
|
||||
|
||||
from testtools.matchers import MatchesRegex
|
||||
|
||||
import zuul.rpcclient
|
||||
|
||||
from tests.base import ZuulTestCase, simple_layout
|
||||
from tests.base import ZuulWebFixture
|
||||
|
||||
|
||||
class TestPagureDriver(ZuulTestCase):
|
||||
config_file = 'zuul-pagure-driver.conf'
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_pull_request_opened(self):
|
||||
|
||||
initial_comment = "This is the\nPR initial_comment."
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project', 'master', 'A', initial_comment=initial_comment)
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test1').result)
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test2').result)
|
||||
|
||||
job = self.getJobFromHistory('project-test2')
|
||||
zuulvars = job.parameters['zuul']
|
||||
self.assertEqual(str(A.number), zuulvars['change'])
|
||||
self.assertEqual(str(A.commit_stop), zuulvars['patchset'])
|
||||
self.assertEqual('master', zuulvars['branch'])
|
||||
self.assertEquals('https://pagure/org/project/pull-request/1',
|
||||
zuulvars['items'][0]['change_url'])
|
||||
self.assertEqual(zuulvars["message"], initial_comment)
|
||||
self.assertEqual(2, len(self.history))
|
||||
self.assertEqual(2, len(A.comments))
|
||||
self.assertEqual(
|
||||
A.comments[0]['comment'], "Starting check jobs.")
|
||||
self.assertThat(
|
||||
A.comments[1]['comment'],
|
||||
MatchesRegex(r'.*\[project-test1 \]\(.*\).*', re.DOTALL))
|
||||
self.assertThat(
|
||||
A.comments[1]['comment'],
|
||||
MatchesRegex(r'.*\[project-test2 \]\(.*\).*', re.DOTALL))
|
||||
self.assertEqual(2, len(A.flags))
|
||||
self.assertEqual('success', A.flags[0]['status'])
|
||||
self.assertEqual('pending', A.flags[1]['status'])
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_pull_request_updated(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest('org/project', 'master', 'A')
|
||||
pr_tip1 = A.commit_stop
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(2, len(self.history))
|
||||
self.assertHistory(
|
||||
[
|
||||
{'name': 'project-test1', 'changes': '1,%s' % pr_tip1},
|
||||
{'name': 'project-test2', 'changes': '1,%s' % pr_tip1},
|
||||
], ordered=False
|
||||
)
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
|
||||
pr_tip2 = A.commit_stop
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(4, len(self.history))
|
||||
self.assertHistory(
|
||||
[
|
||||
{'name': 'project-test1', 'changes': '1,%s' % pr_tip1},
|
||||
{'name': 'project-test2', 'changes': '1,%s' % pr_tip1},
|
||||
{'name': 'project-test1', 'changes': '1,%s' % pr_tip2},
|
||||
{'name': 'project-test2', 'changes': '1,%s' % pr_tip2}
|
||||
], ordered=False
|
||||
)
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_pull_request_updated_builds_aborted(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest('org/project', 'master', 'A')
|
||||
pr_tip1 = A.commit_stop
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
|
||||
pr_tip2 = A.commit_stop
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertHistory(
|
||||
[
|
||||
{'name': 'project-test1', 'result': 'ABORTED',
|
||||
'changes': '1,%s' % pr_tip1},
|
||||
{'name': 'project-test2', 'result': 'ABORTED',
|
||||
'changes': '1,%s' % pr_tip1},
|
||||
{'name': 'project-test1', 'changes': '1,%s' % pr_tip2},
|
||||
{'name': 'project-test2', 'changes': '1,%s' % pr_tip2}
|
||||
], ordered=False
|
||||
)
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_pull_request_commented(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest('org/project', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(2, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent('I like that change'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(2, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent('recheck'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(4, len(self.history))
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_pull_request_with_dyn_reconf(self):
|
||||
|
||||
zuul_yaml = [
|
||||
{'job': {
|
||||
'name': 'project-test3',
|
||||
'run': 'job.yaml'
|
||||
}},
|
||||
{'project': {
|
||||
'check': {
|
||||
'jobs': [
|
||||
'project-test3'
|
||||
]
|
||||
}
|
||||
}}
|
||||
]
|
||||
playbook = "- hosts: all\n tasks: []"
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project', 'master', 'A')
|
||||
A.addCommit(
|
||||
{'.zuul.yaml': yaml.dump(zuul_yaml),
|
||||
'job.yaml': playbook}
|
||||
)
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test1').result)
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test2').result)
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test3').result)
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_ref_updated(self):
|
||||
|
||||
event = self.fake_pagure.getGitReceiveEvent('org/project')
|
||||
expected_newrev = event[1]['msg']['stop_commit']
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(1, len(self.history))
|
||||
self.assertEqual(
|
||||
'SUCCESS',
|
||||
self.getJobFromHistory('project-post-job').result)
|
||||
|
||||
job = self.getJobFromHistory('project-post-job')
|
||||
zuulvars = job.parameters['zuul']
|
||||
self.assertEqual('refs/heads/master', zuulvars['ref'])
|
||||
self.assertEqual('post', zuulvars['pipeline'])
|
||||
self.assertEqual('project-post-job', zuulvars['job'])
|
||||
self.assertEqual('master', zuulvars['branch'])
|
||||
self.assertEqual(
|
||||
'https://pagure/org/project/commit/%s' % zuulvars['newrev'],
|
||||
zuulvars['change_url'])
|
||||
self.assertEqual(expected_newrev, zuulvars['newrev'])
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_ref_updated_and_tenant_reconfigure(self):
|
||||
|
||||
self.waitUntilSettled()
|
||||
old = self.sched.tenant_last_reconfigured.get('tenant-one', 0)
|
||||
time.sleep(1)
|
||||
|
||||
zuul_yaml = [
|
||||
{'job': {
|
||||
'name': 'project-post-job2',
|
||||
'run': 'job.yaml'
|
||||
}},
|
||||
{'project': {
|
||||
'post': {
|
||||
'jobs': [
|
||||
'project-post-job2'
|
||||
]
|
||||
}
|
||||
}}
|
||||
]
|
||||
playbook = "- hosts: all\n tasks: []"
|
||||
self.create_commit(
|
||||
'org/project',
|
||||
{'.zuul.yaml': yaml.dump(zuul_yaml),
|
||||
'job.yaml': playbook},
|
||||
message='Add InRepo configuration'
|
||||
)
|
||||
event = self.fake_pagure.getGitReceiveEvent('org/project')
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
new = self.sched.tenant_last_reconfigured.get('tenant-one', 0)
|
||||
# New timestamp should be greater than the old timestamp
|
||||
self.assertLess(old, new)
|
||||
|
||||
self.assertHistory(
|
||||
[{'name': 'project-post-job'},
|
||||
{'name': 'project-post-job2'},
|
||||
], ordered=False
|
||||
)
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_client_dequeue_change_pagure(self):
|
||||
|
||||
client = zuul.rpcclient.RPCClient('127.0.0.1',
|
||||
self.gearman_server.port)
|
||||
self.addCleanup(client.shutdown)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
A = self.fake_pagure.openFakePullRequest('org/project', 'master', 'A')
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
client.dequeue(
|
||||
tenant='tenant-one',
|
||||
pipeline='check',
|
||||
project='org/project',
|
||||
change='%s,%s' % (A.number, A.commit_stop),
|
||||
ref=None)
|
||||
|
||||
self.waitUntilSettled()
|
||||
|
||||
tenant = self.sched.abide.tenants.get('tenant-one')
|
||||
check_pipeline = tenant.layout.pipelines['check']
|
||||
self.assertEqual(check_pipeline.getAllItems(), [])
|
||||
self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 2)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_client_enqueue_change_pagure(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest('org/project', 'master', 'A')
|
||||
|
||||
client = zuul.rpcclient.RPCClient('127.0.0.1',
|
||||
self.gearman_server.port)
|
||||
self.addCleanup(client.shutdown)
|
||||
r = client.enqueue(tenant='tenant-one',
|
||||
pipeline='check',
|
||||
project='org/project',
|
||||
trigger='pagure',
|
||||
change='%s,%s' % (A.number, A.commit_stop))
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual(self.getJobFromHistory('project-test1').result,
|
||||
'SUCCESS')
|
||||
self.assertEqual(self.getJobFromHistory('project-test2').result,
|
||||
'SUCCESS')
|
||||
self.assertEqual(r, True)
|
||||
|
||||
@simple_layout('layouts/requirements-pagure.yaml', driver='pagure')
|
||||
def test_pr_score_require_1_vote(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project1', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent("I like that change"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(1, len(self.history))
|
||||
|
||||
self.assertEqual(
|
||||
'SUCCESS',
|
||||
self.getJobFromHistory('project-test').result)
|
||||
|
||||
@simple_layout('layouts/requirements-pagure.yaml', driver='pagure')
|
||||
def test_pr_score_require_2_votes(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project2', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent("I like that change"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsdown:"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(1, len(self.history))
|
||||
|
||||
@simple_layout('layouts/requirements-pagure.yaml', driver='pagure')
|
||||
def test_status_trigger(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project3', 'master', 'A')
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestStatusSetEvent("failure"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestStatusSetEvent("success"))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(1, len(self.history))
|
||||
|
||||
@simple_layout('layouts/merging-pagure.yaml', driver='pagure')
|
||||
def test_merge_action_in_independent(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project1', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual(1, len(self.history))
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test').result)
|
||||
self.assertEqual('Merged', A.status)
|
||||
|
||||
@simple_layout('layouts/merging-pagure.yaml', driver='pagure')
|
||||
def test_merge_action_in_dependent(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project2', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
# connection.canMerge is not validated
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
# Set the mergeable PR flag to a expected value
|
||||
A.cached_merge_status = 'MERGE'
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
# connection.canMerge is not validated
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
# Set the score threshold as reached
|
||||
A.threshold_reached = True
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
# connection.canMerge is not validated
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
# Set CI flag as passed CI
|
||||
A.addFlag('success', 'https://url', 'Build passed')
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent())
|
||||
self.waitUntilSettled()
|
||||
# connection.canMerge is validated
|
||||
self.assertEqual(1, len(self.history))
|
||||
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test').result)
|
||||
self.assertEqual('Merged', A.status)
|
||||
|
||||
@simple_layout('layouts/crd-pagure.yaml', driver='pagure')
|
||||
def test_crd_independent(self):
|
||||
|
||||
# Create a change in project1 that a project2 change will depend on
|
||||
A = self.fake_pagure.openFakePullRequest('org/project1', 'master', 'A')
|
||||
|
||||
# Create a commit in B that sets the dependency on A
|
||||
msg = "Depends-On: %s" % A.url
|
||||
B = self.fake_pagure.openFakePullRequest(
|
||||
'org/project2', 'master', 'B', initial_comment=msg)
|
||||
|
||||
# Make an event to re-use
|
||||
event = B.getPullRequestCommentedEvent('A comment')
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
# The changes for the job from project2 should include the project1
|
||||
# PR content
|
||||
changes = self.getJobFromHistory(
|
||||
'project2-test', 'org/project2').changes
|
||||
|
||||
self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
|
||||
A.commit_stop,
|
||||
B.number,
|
||||
B.commit_stop))
|
||||
|
||||
# There should be no more changes in the queue
|
||||
tenant = self.sched.abide.tenants.get('tenant-one')
|
||||
self.assertEqual(len(tenant.layout.pipelines['check'].queues), 0)
|
||||
|
||||
@simple_layout('layouts/crd-pagure.yaml', driver='pagure')
|
||||
def test_crd_dependent(self):
|
||||
|
||||
# Create a change in project3 that a project4 change will depend on
|
||||
A = self.fake_pagure.openFakePullRequest('org/project3', 'master', 'A')
|
||||
|
||||
# Create a commit in B that sets the dependency on A
|
||||
msg = "Depends-On: %s" % A.url
|
||||
B = self.fake_pagure.openFakePullRequest(
|
||||
'org/project4', 'master', 'B', initial_comment=msg)
|
||||
|
||||
# Make an event to re-use
|
||||
event = B.getPullRequestCommentedEvent('A comment')
|
||||
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
# Neither A and B can't merge (no flag, no score threshold)
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
B.threshold_reached = True
|
||||
B.addFlag('success', 'https://url', 'Build passed')
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
# B can't merge as A got no flag, no score threshold
|
||||
self.assertEqual(0, len(self.history))
|
||||
|
||||
A.threshold_reached = True
|
||||
A.addFlag('success', 'https://url', 'Build passed')
|
||||
self.fake_pagure.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
# The changes for the job from project4 should include the project3
|
||||
# PR content
|
||||
changes = self.getJobFromHistory(
|
||||
'project4-test', 'org/project4').changes
|
||||
|
||||
self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
|
||||
A.commit_stop,
|
||||
B.number,
|
||||
B.commit_stop))
|
||||
|
||||
self.assertTrue(A.is_merged)
|
||||
self.assertTrue(B.is_merged)
|
||||
|
||||
@simple_layout('layouts/crd-pagure.yaml', driver='pagure')
|
||||
def test_crd_needed_changes(self):
|
||||
|
||||
# Given change A and B, where B depends on A, when A
|
||||
# completes B should be enqueued (using a shared queue)
|
||||
|
||||
# Create a change in project3 that a project4 change will depend on
|
||||
A = self.fake_pagure.openFakePullRequest('org/project3', 'master', 'A')
|
||||
A.threshold_reached = True
|
||||
A.addFlag('success', 'https://url', 'Build passed')
|
||||
|
||||
# Set B to depend on A
|
||||
msg = "Depends-On: %s" % A.url
|
||||
B = self.fake_pagure.openFakePullRequest(
|
||||
'org/project4', 'master', 'B', initial_comment=msg)
|
||||
# Make the driver aware of change B by sending an event
|
||||
# At that moment B can't merge
|
||||
self.fake_pagure.emitEvent(B.getPullRequestCommentedEvent('A comment'))
|
||||
# Now set B mergeable
|
||||
B.threshold_reached = True
|
||||
B.addFlag('success', 'https://url', 'Build passed')
|
||||
|
||||
# Enqueue A, which will make the scheduler detect that B is
|
||||
# depending on so B will be enqueue as well.
|
||||
self.fake_pagure.emitEvent(A.getPullRequestCommentedEvent('A comment'))
|
||||
self.waitUntilSettled()
|
||||
|
||||
# The changes for the job from project4 should include the project3
|
||||
# PR content
|
||||
changes = self.getJobFromHistory(
|
||||
'project4-test', 'org/project4').changes
|
||||
|
||||
self.assertEqual(changes, "%s,%s %s,%s" % (A.number,
|
||||
A.commit_stop,
|
||||
B.number,
|
||||
B.commit_stop))
|
||||
|
||||
self.assertTrue(A.is_merged)
|
||||
self.assertTrue(B.is_merged)
|
||||
|
||||
|
||||
class TestPagureToGerritCRD(ZuulTestCase):
|
||||
config_file = 'zuul-crd-pagure.conf'
|
||||
tenant_config_file = 'config/cross-source-pagure/gerrit.yaml'
|
||||
|
||||
def test_crd_gate(self):
|
||||
"Test cross-repo dependencies"
|
||||
A = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'A')
|
||||
B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
|
||||
|
||||
# A Depends-On: B
|
||||
A.editInitialComment('Depends-On: %s\n' % (B.data['url']))
|
||||
|
||||
A.addFlag('success', 'https://url', 'Build passed')
|
||||
A.threshold_reached = True
|
||||
|
||||
B.addApproval('Code-Review', 2)
|
||||
|
||||
# Make A enter the pipeline
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
|
||||
# Expect not merged as B not approved yet
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertEqual(B.data['status'], 'NEW')
|
||||
|
||||
for connection in self.connections.connections.values():
|
||||
connection.maintainCache([])
|
||||
|
||||
B.addApproval('Approved', 1)
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(A.is_merged)
|
||||
self.assertEqual(B.data['status'], 'MERGED')
|
||||
self.assertEqual(len(A.comments), 4)
|
||||
self.assertEqual(B.reported, 2)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'pagure/project2').changes
|
||||
self.assertEqual(changes, '1,1 1,%s' % A.commit_stop)
|
||||
|
||||
def test_crd_check(self):
|
||||
"Test cross-repo dependencies in independent pipelines"
|
||||
A = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'A')
|
||||
B = self.fake_gerrit.addFakeChange(
|
||||
'gerrit/project1', 'master', 'B')
|
||||
|
||||
# A Depends-On: B
|
||||
A.editInitialComment('Depends-On: %s\n' % (B.data['url'],))
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(self.builds[0].hasChanges(A, B))
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertEqual(B.data['status'], 'NEW')
|
||||
self.assertEqual(len(A.comments), 2)
|
||||
self.assertEqual(B.reported, 0)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'pagure/project2').changes
|
||||
self.assertEqual(changes, '1,1 1,%s' % A.commit_stop)
|
||||
|
||||
|
||||
class TestGerritToPagureCRD(ZuulTestCase):
|
||||
config_file = 'zuul-crd-pagure.conf'
|
||||
tenant_config_file = 'config/cross-source-pagure/gerrit.yaml'
|
||||
|
||||
def test_crd_gate(self):
|
||||
"Test cross-repo dependencies"
|
||||
A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
|
||||
B = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'B')
|
||||
|
||||
A.addApproval('Code-Review', 2)
|
||||
|
||||
AM2 = self.fake_gerrit.addFakeChange('gerrit/project1', 'master',
|
||||
'AM2')
|
||||
AM1 = self.fake_gerrit.addFakeChange('gerrit/project1', 'master',
|
||||
'AM1')
|
||||
AM2.setMerged()
|
||||
AM1.setMerged()
|
||||
|
||||
# A -> AM1 -> AM2
|
||||
# A Depends-On: B
|
||||
# M2 is here to make sure it is never queried. If it is, it
|
||||
# means zuul is walking down the entire history of merged
|
||||
# changes.
|
||||
|
||||
A.setDependsOn(AM1, 1)
|
||||
AM1.setDependsOn(AM2, 1)
|
||||
|
||||
A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
|
||||
A.subject, B.url)
|
||||
|
||||
self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual(A.data['status'], 'NEW')
|
||||
self.assertFalse(B.is_merged)
|
||||
|
||||
for connection in self.connections.connections.values():
|
||||
connection.maintainCache([])
|
||||
|
||||
B.addFlag('success', 'https://url', 'Build passed')
|
||||
B.threshold_reached = True
|
||||
self.fake_pagure.emitEvent(
|
||||
B.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
|
||||
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual(AM2.queried, 0)
|
||||
self.assertEqual(A.data['status'], 'MERGED')
|
||||
self.assertTrue(B.is_merged)
|
||||
self.assertEqual(A.reported, 2)
|
||||
self.assertEqual(len(B.comments), 3)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'gerrit/project1').changes
|
||||
self.assertEqual(changes, '1,%s 1,1' % B.commit_stop)
|
||||
|
||||
def test_crd_check(self):
|
||||
"Test cross-repo dependencies in independent pipelines"
|
||||
A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
|
||||
B = self.fake_pagure.openFakePullRequest(
|
||||
'pagure/project2', 'master', 'B')
|
||||
|
||||
# A Depends-On: B
|
||||
A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
|
||||
A.subject, B.url)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(self.builds[0].hasChanges(A, B))
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual(A.data['status'], 'NEW')
|
||||
self.assertFalse(B.is_merged)
|
||||
self.assertEqual(A.reported, 1)
|
||||
self.assertEqual(len(B.comments), 0)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'gerrit/project1').changes
|
||||
self.assertEqual(changes, '1,%s 1,1' % B.commit_stop)
|
||||
|
||||
|
||||
class TestPagureToGithubCRD(ZuulTestCase):
|
||||
config_file = 'zuul-crd-pagure.conf'
|
||||
tenant_config_file = 'config/cross-source-pagure/github.yaml'
|
||||
|
||||
def test_crd_gate(self):
|
||||
"Test cross-repo dependencies"
|
||||
A = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'A')
|
||||
B = self.fake_github.openFakePullRequest('github/project1', 'master',
|
||||
'B')
|
||||
# A Depends-On: B
|
||||
A.editInitialComment('Depends-On: %s\n' % (B.url))
|
||||
|
||||
A.addFlag('success', 'https://url', 'Build passed')
|
||||
A.threshold_reached = True
|
||||
|
||||
# Make A enter the pipeline
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
|
||||
# Expect not merged as B not approved yet
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertFalse(B.is_merged)
|
||||
|
||||
for connection in self.connections.connections.values():
|
||||
connection.maintainCache([])
|
||||
|
||||
B.addLabel('approved')
|
||||
self.fake_pagure.emitEvent(
|
||||
A.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(A.is_merged)
|
||||
self.assertTrue(B.is_merged)
|
||||
self.assertEqual(len(A.comments), 4)
|
||||
self.assertEqual(len(B.comments), 2)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'pagure/project2').changes
|
||||
self.assertEqual(changes, '1,%s 1,%s' % (B.head_sha, A.commit_stop))
|
||||
|
||||
def test_crd_check(self):
|
||||
"Test cross-repo dependencies in independent pipelines"
|
||||
A = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'A')
|
||||
B = self.fake_github.openFakePullRequest('github/project1', 'master',
|
||||
'A')
|
||||
|
||||
# A Depends-On: B
|
||||
A.editInitialComment('Depends-On: %s\n' % B.url)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(self.builds[0].hasChanges(A, B))
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertFalse(B.is_merged)
|
||||
self.assertEqual(len(A.comments), 2)
|
||||
self.assertEqual(len(A.comments), 2)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'pagure/project2').changes
|
||||
self.assertEqual(changes, '1,%s 1,%s' % (B.head_sha, A.commit_stop))
|
||||
|
||||
|
||||
class TestGithubToPagureCRD(ZuulTestCase):
|
||||
config_file = 'zuul-crd-pagure.conf'
|
||||
tenant_config_file = 'config/cross-source-pagure/github.yaml'
|
||||
|
||||
def test_crd_gate(self):
|
||||
"Test cross-repo dependencies"
|
||||
A = self.fake_github.openFakePullRequest('github/project1', 'master',
|
||||
'A')
|
||||
B = self.fake_pagure.openFakePullRequest('pagure/project2', 'master',
|
||||
'B')
|
||||
|
||||
# A Depends-On: B
|
||||
A.editBody('Depends-On: %s\n' % B.url)
|
||||
|
||||
event = A.addLabel('approved')
|
||||
self.fake_github.emitEvent(event)
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertFalse(B.is_merged)
|
||||
|
||||
for connection in self.connections.connections.values():
|
||||
connection.maintainCache([])
|
||||
|
||||
B.addFlag('success', 'https://url', 'Build passed')
|
||||
B.threshold_reached = True
|
||||
self.fake_pagure.emitEvent(
|
||||
B.getPullRequestCommentedEvent(":thumbsup:"))
|
||||
|
||||
self.fake_github.emitEvent(event)
|
||||
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(A.is_merged)
|
||||
self.assertTrue(B.is_merged)
|
||||
self.assertEqual(len(A.comments), 2)
|
||||
self.assertEqual(len(B.comments), 3)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'github/project1').changes
|
||||
self.assertEqual(changes, '1,%s 1,%s' % (B.commit_stop, A.head_sha))
|
||||
|
||||
def test_crd_check(self):
|
||||
"Test cross-repo dependencies in independent pipelines"
|
||||
A = self.fake_github.openFakePullRequest(
|
||||
'github/project1', 'master', 'A')
|
||||
B = self.fake_pagure.openFakePullRequest(
|
||||
'pagure/project2', 'master', 'B')
|
||||
|
||||
# A Depends-On: B
|
||||
A.editBody('Depends-On: %s\n' % B.url)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.fake_github.emitEvent(A.getPullRequestEditedEvent())
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertTrue(self.builds[0].hasChanges(A, B))
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertFalse(A.is_merged)
|
||||
self.assertFalse(B.is_merged)
|
||||
self.assertEqual(len(A.comments), 1)
|
||||
self.assertEqual(len(B.comments), 0)
|
||||
|
||||
changes = self.getJobFromHistory(
|
||||
'project-merge', 'github/project1').changes
|
||||
self.assertEqual(changes, '1,%s 1,%s' % (B.commit_stop, A.head_sha))
|
||||
|
||||
|
||||
class TestPagureWebhook(ZuulTestCase):
|
||||
config_file = 'zuul-pagure-driver.conf'
|
||||
|
||||
def setUp(self):
|
||||
super(TestPagureWebhook, self).setUp()
|
||||
|
||||
# Start the web server
|
||||
self.web = self.useFixture(
|
||||
ZuulWebFixture(self.gearman_server.port,
|
||||
self.config))
|
||||
|
||||
host = '127.0.0.1'
|
||||
# Wait until web server is started
|
||||
while True:
|
||||
port = self.web.port
|
||||
try:
|
||||
with socket.create_connection((host, port)):
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
pass
|
||||
|
||||
self.fake_pagure.setZuulWebPort(port)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestPagureWebhook, self).tearDown()
|
||||
|
||||
@simple_layout('layouts/basic-pagure.yaml', driver='pagure')
|
||||
def test_webhook(self):
|
||||
|
||||
A = self.fake_pagure.openFakePullRequest(
|
||||
'org/project', 'master', 'A')
|
||||
self.fake_pagure.emitEvent(A.getPullRequestOpenedEvent(),
|
||||
use_zuulweb=True,
|
||||
project='org/project')
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test1').result)
|
||||
self.assertEqual('SUCCESS',
|
||||
self.getJobFromHistory('project-test2').result)
|
|
@ -97,7 +97,8 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
|
|||
try:
|
||||
self.configure_connections(
|
||||
include_drivers=[zuul.driver.sql.SQLDriver,
|
||||
zuul.driver.github.GithubDriver])
|
||||
zuul.driver.github.GithubDriver,
|
||||
zuul.driver.pagure.PagureDriver])
|
||||
self._run()
|
||||
except Exception:
|
||||
self.log.exception("Exception from WebServer:")
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from zuul.driver import Driver, ConnectionInterface, TriggerInterface
|
||||
from zuul.driver import SourceInterface, ReporterInterface
|
||||
from zuul.driver.pagure import pagureconnection
|
||||
from zuul.driver.pagure import paguresource
|
||||
from zuul.driver.pagure import pagurereporter
|
||||
from zuul.driver.pagure import paguretrigger
|
||||
|
||||
|
||||
class PagureDriver(Driver, ConnectionInterface, TriggerInterface,
|
||||
SourceInterface, ReporterInterface):
|
||||
name = 'pagure'
|
||||
|
||||
def getConnection(self, name, config):
|
||||
return pagureconnection.PagureConnection(self, name, config)
|
||||
|
||||
def getTrigger(self, connection, config=None):
|
||||
return paguretrigger.PagureTrigger(self, connection, config)
|
||||
|
||||
def getSource(self, connection):
|
||||
return paguresource.PagureSource(self, connection)
|
||||
|
||||
def getReporter(self, connection, pipeline, config=None):
|
||||
return pagurereporter.PagureReporter(
|
||||
self, connection, pipeline, config)
|
||||
|
||||
def getTriggerSchema(self):
|
||||
return paguretrigger.getSchema()
|
||||
|
||||
def getReporterSchema(self):
|
||||
return pagurereporter.getSchema()
|
||||
|
||||
def getRequireSchema(self):
|
||||
return paguresource.getRequireSchema()
|
||||
|
||||
def getRejectSchema(self):
|
||||
return paguresource.getRejectSchema()
|
|
@ -0,0 +1,886 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
import cherrypy
|
||||
import traceback
|
||||
import voluptuous as v
|
||||
|
||||
import gear
|
||||
|
||||
from zuul.connection import BaseConnection
|
||||
from zuul.web.handler import BaseWebController
|
||||
from zuul.lib.config import get_default
|
||||
from zuul.model import Ref, Branch, Tag
|
||||
from zuul.lib import dependson
|
||||
|
||||
from zuul.driver.pagure.paguremodel import PagureTriggerEvent, PullRequest
|
||||
|
||||
# Minimal Pagure version supported 5.3.0
|
||||
#
|
||||
# Pagure is similar to Github as it handles Pullrequest where PR is a branch
|
||||
# composed of one or more commits. A PR can be commented, evaluated, updated,
|
||||
# CI flagged, and merged. A PR can be flagged (success/failure/pending) and
|
||||
# this driver use that capability. Code review (evaluation) is done via
|
||||
# comments that contains a :thumbsup: or :thumbsdown:. Pagure computes a
|
||||
# score based on that and allow or not the merge of PR if the "minimal score to
|
||||
# merge" is set in repository settings. This driver uses that setting and need
|
||||
# to be set. This driver expects to receive repository events via webhooks and
|
||||
# expects to verify payload signature. The driver connection needs an user's
|
||||
# API key with the "Modify an existing project" access. This user needs to be
|
||||
# added as admin against projects to be gated by Zuul.
|
||||
#
|
||||
# The web hook target must be (in repository settings):
|
||||
# - http://<zuul-web>/zuul/api/connection/<conn-name>/payload
|
||||
#
|
||||
# Repository settings (to be checked):
|
||||
# - Always merge (Better to match internal merge strategy of Zuul)
|
||||
# - Minimum score to merge pull-request
|
||||
# - Notify on pull-request flag
|
||||
# - Pull requests
|
||||
#
|
||||
# To define the connection in /etc/zuul/zuul.conf:
|
||||
# [connection pagure.sftests.com]
|
||||
# driver=pagure
|
||||
# server=pagure.sftests.com
|
||||
# baseurl=https://pagure.sftests.com/pagure
|
||||
# cloneurl=https://pagure.sftests.com/pagure/git
|
||||
# api_token=QX29SXAW96C2CTLUNA5JKEEU65INGWTO2B5NHBDBRMF67S7PYZWCS0L1AKHXXXXX
|
||||
#
|
||||
# Current Non blocking issues:
|
||||
# - Pagure does not send event when git tag is added/removed
|
||||
# https://pagure.io/pagure/issue/4400 (merged so need to be used)
|
||||
# - Pagure does not send the oldrev info when a branch is updated/created
|
||||
# https://pagure.io/pagure/issue/4401
|
||||
# - Pagure does not send an event when a branch is deleted
|
||||
# https://pagure.io/pagure/issue/4399 (merged so need to be used)
|
||||
# - Pagure does not reset the score when a PR code is updated
|
||||
# https://pagure.io/pagure/issue/3985
|
||||
# Pagure does not send an event when initial_comment is updated
|
||||
# https://pagure.io/pagure/issue/4398 (merged need to be used)
|
||||
# - CI status flag updated field unit is second, better to have millisecond
|
||||
# unit to avoid unpossible sorting to get last status if two status set the
|
||||
# same second.
|
||||
# https://pagure.io/pagure/issue/4402
|
||||
# - Zuul needs to be able to search commits that set a dependency (depends-on)
|
||||
# to a specific commit to reset jobs run when a dependency is changed. On
|
||||
# Gerrit and Github search through commits message is possible and used by
|
||||
# Zuul. Pagure does not offer this capability.
|
||||
|
||||
# Side notes
|
||||
# - Idea would be to prevent PR merge by anybody else than Zuul.
|
||||
# Pagure project option: "Activate Only assignee can merge pull-request"
|
||||
# https://docs.pagure.org/pagure/usage/project_settings.html?highlight=score#activate-only-assignee-can-merge-pull-request
|
||||
|
||||
|
||||
def _sign_request(body, secret):
|
||||
signature = hmac.new(
|
||||
secret.encode('utf-8'), body, hashlib.sha1).hexdigest()
|
||||
return signature, body
|
||||
|
||||
|
||||
class PagureGearmanWorker(object):
|
||||
"""A thread that answers gearman requests"""
|
||||
log = logging.getLogger("zuul.PagureGearmanWorker")
|
||||
|
||||
def __init__(self, connection):
|
||||
self.config = connection.sched.config
|
||||
self.connection = connection
|
||||
self.thread = threading.Thread(target=self._run,
|
||||
name='pagure-gearman-worker')
|
||||
self._running = False
|
||||
handler = "pagure:%s:payload" % self.connection.connection_name
|
||||
self.jobs = {
|
||||
handler: self.handle_payload,
|
||||
}
|
||||
|
||||
def _run(self):
|
||||
while self._running:
|
||||
try:
|
||||
job = self.gearman.getJob()
|
||||
try:
|
||||
if job.name not in self.jobs:
|
||||
self.log.exception("Exception while running job")
|
||||
job.sendWorkException(
|
||||
traceback.format_exc().encode('utf8'))
|
||||
continue
|
||||
output = self.jobs[job.name](json.loads(job.arguments))
|
||||
job.sendWorkComplete(json.dumps(output))
|
||||
except Exception:
|
||||
self.log.exception("Exception while running job")
|
||||
job.sendWorkException(
|
||||
traceback.format_exc().encode('utf8'))
|
||||
except gear.InterruptedError:
|
||||
pass
|
||||
except Exception:
|
||||
self.log.exception("Exception while getting job")
|
||||
|
||||
def handle_payload(self, args):
|
||||
payload = args["payload"]
|
||||
|
||||
self.log.info(
|
||||
"Pagure Webhook Received (id: %(msg_id)s, topic: %(topic)s)" % (
|
||||
payload))
|
||||
|
||||
try:
|
||||
self.__dispatch_event(payload)
|
||||
output = {'return_code': 200}
|
||||
except Exception:
|
||||
output = {'return_code': 503}
|
||||
self.log.exception("Exception handling Pagure event:")
|
||||
|
||||
return output
|
||||
|
||||
def __dispatch_event(self, payload):
|
||||
event = payload['topic']
|
||||
try:
|
||||
self.log.info("Dispatching event %s" % event)
|
||||
self.connection.addEvent(payload, event)
|
||||
except Exception as err:
|
||||
message = 'Exception dispatching event: %s' % str(err)
|
||||
self.log.exception(message)
|
||||
raise Exception(message)
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
server = self.config.get('gearman', 'server')
|
||||
port = get_default(self.config, 'gearman', 'port', 4730)
|
||||
ssl_key = get_default(self.config, 'gearman', 'ssl_key')
|
||||
ssl_cert = get_default(self.config, 'gearman', 'ssl_cert')
|
||||
ssl_ca = get_default(self.config, 'gearman', 'ssl_ca')
|
||||
self.gearman = gear.TextWorker('Zuul Pagure Connector')
|
||||
self.log.debug("Connect to gearman")
|
||||
self.gearman.addServer(server, port, ssl_key, ssl_cert, ssl_ca)
|
||||
self.log.debug("Waiting for server")
|
||||
self.gearman.waitForServer()
|
||||
self.log.debug("Registering")
|
||||
for job in self.jobs:
|
||||
self.gearman.registerFunction(job)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self.gearman.stopWaitingForJobs()
|
||||
# We join here to avoid whitelisting the thread -- if it takes more
|
||||
# than 5s to stop in tests, there's a problem.
|
||||
self.thread.join(timeout=5)
|
||||
self.gearman.shutdown()
|
||||
|
||||
|
||||
class PagureEventConnector(threading.Thread):
|
||||
"""Move events from Pagure into the scheduler"""
|
||||
|
||||
log = logging.getLogger("zuul.PagureEventConnector")
|
||||
|
||||
def __init__(self, connection):
|
||||
super(PagureEventConnector, self).__init__()
|
||||
self.daemon = True
|
||||
self.connection = connection
|
||||
self._stopped = False
|
||||
self.event_handler_mapping = {
|
||||
'pull-request.comment.added': self._event_issue_comment,
|
||||
'pull-request.new': self._event_pull_request,
|
||||
'pull-request.flag.added': self._event_flag_added,
|
||||
'git.receive': self._event_ref_updated,
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
self._stopped = True
|
||||
self.connection.addEvent(None)
|
||||
|
||||
def _handleEvent(self):
|
||||
ts, json_body, event_type = self.connection.getEvent()
|
||||
if self._stopped:
|
||||
return
|
||||
|
||||
self.log.info("Received event: %s" % str(event_type))
|
||||
# self.log.debug("Event payload: %s " % json_body)
|
||||
|
||||
if event_type not in self.event_handler_mapping:
|
||||
message = "Unhandled X-Pagure-Event: %s" % event_type
|
||||
self.log.info(message)
|
||||
return
|
||||
|
||||
if event_type in self.event_handler_mapping:
|
||||
self.log.debug("Handling event: %s" % event_type)
|
||||
|
||||
try:
|
||||
event = self.event_handler_mapping[event_type](json_body)
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
'Exception when handling event: %s' % event_type)
|
||||
event = None
|
||||
|
||||
if event:
|
||||
if event.change_number:
|
||||
project = self.connection.source.getProject(event.project_name)
|
||||
self.connection._getChange(project,
|
||||
event.change_number,
|
||||
event.patch_number,
|
||||
refresh=True,
|
||||
url=event.change_url,
|
||||
event=event)
|
||||
event.project_hostname = self.connection.canonical_hostname
|
||||
self.connection.logEvent(event)
|
||||
self.connection.sched.addEvent(event)
|
||||
|
||||
def _event_base(self, body):
|
||||
event = PagureTriggerEvent()
|
||||
if 'pullrequest' in body['msg']:
|
||||
data = body['msg']['pullrequest']
|
||||
data['flag'] = body['msg'].get('flag')
|
||||
event.title = data.get('title')
|
||||
event.project_name = data.get('project', {}).get('fullname')
|
||||
event.change_number = data.get('id')
|
||||
event.updated_at = data.get('date_created')
|
||||
event.branch = data.get('branch')
|
||||
event.change_url = self.connection.getPullUrl(event.project_name,
|
||||
event.change_number)
|
||||
event.ref = "refs/pull/%s/head" % event.change_number
|
||||
# commit_stop is the tip of the PR branch
|
||||
event.patch_number = data.get('commit_stop')
|
||||
event.type = 'pg_pull_request'
|
||||
else:
|
||||
data = body['msg']
|
||||
event.type = 'pg_push'
|
||||
return event, data
|
||||
|
||||
def _event_issue_comment(self, body):
|
||||
""" Handles pull request comments """
|
||||
# https://fedora-fedmsg.readthedocs.io/en/latest/topics.html#pagure-pull-request-comment-added
|
||||
event, data = self._event_base(body)
|
||||
last_comment = data.get('comments', [])[-1]
|
||||
if last_comment.get('notification') is True:
|
||||
# An updated PR (new commits) triggers the comment.added
|
||||
# event. A message is added by pagure on the PR but notification
|
||||
# is set to true.
|
||||
event.action = 'changed'
|
||||
else:
|
||||
if last_comment.get('comment', '').find(':thumbsup:') >= 0:
|
||||
event.action = 'thumbsup'
|
||||
event.type = 'pg_pull_request_review'
|
||||
elif last_comment.get('comment', '').find(':thumbsdown:') >= 0:
|
||||
event.action = 'thumbsdown'
|
||||
event.type = 'pg_pull_request_review'
|
||||
else:
|
||||
event.action = 'comment'
|
||||
# Assume last comment is the one that have triggered the event
|
||||
event.comment = last_comment.get('comment')
|
||||
return event
|
||||
|
||||
def _event_pull_request(self, body):
|
||||
""" Handles pull request event """
|
||||
# https://fedora-fedmsg.readthedocs.io/en/latest/topics.html#pagure-pull-request-new
|
||||
event, data = self._event_base(body)
|
||||
event.action = 'opened'
|
||||
return event
|
||||
|
||||
def _event_flag_added(self, body):
|
||||
""" Handles flag added event """
|
||||
# https://fedora-fedmsg.readthedocs.io/en/latest/topics.html#pagure-pull-request-flag-added
|
||||
event, data = self._event_base(body)
|
||||
event.status = data['flag']['status']
|
||||
event.action = 'status'
|
||||
return event
|
||||
|
||||
def _event_ref_updated(self, body):
|
||||
""" Handles ref updated """
|
||||
# https://fedora-fedmsg.readthedocs.io/en/latest/topics.html#git-receive
|
||||
event, data = self._event_base(body)
|
||||
event.project_name = data.get('project_fullname')
|
||||
event.branch = data.get('branch')
|
||||
event.ref = 'refs/heads/%s' % event.branch
|
||||
event.newrev = data.get('end_commit', data.get('stop_commit'))
|
||||
# There is no concept of old rev (that is the previous branch tip) in
|
||||
# pagure. end_commit is the new tip, start_commit is the oldest
|
||||
# commit on the branch merged. stop_commit is the youngest commit
|
||||
# on the branch. When a PR is merged (with a merge commit) end_commit
|
||||
# is the merge commit sha, start and stop commits are the boundaries
|
||||
# of the branch.
|
||||
|
||||
# Then do not set oldrev as this information is missing
|
||||
# event.oldrev = data.get('start_commit')
|
||||
event.branch_updated = True
|
||||
|
||||
# TODO(fbo): Pagure sends an event when a branch is created but the
|
||||
# old rev info is not set by pagure. A new branch will be handled
|
||||
# as ref updated. https://pagure.io/pagure/issue/4401
|
||||
# TODO(fbo): Pagure does not send an event when a branch is deleted
|
||||
# https://pagure.io/pagure/issue/4399
|
||||
|
||||
# if event.oldrev == '0' * 40:
|
||||
# event.branch_created = True
|
||||
# if event.newrev == '0' * 40:
|
||||
# event.branch_deleted = True
|
||||
#
|
||||
# if event.branch:
|
||||
# project = self.connection.source.getProject(event.project_name)
|
||||
# if event.branch_deleted:
|
||||
# self.connection.project_branch_cache[project].remove(
|
||||
# event.branch)
|
||||
# elif event.branch_created:
|
||||
# self.connection.project_branch_cache[project].append(
|
||||
# event.branch)
|
||||
# else:
|
||||
# pass
|
||||
|
||||
return event
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self._stopped:
|
||||
return
|
||||
try:
|
||||
self._handleEvent()
|
||||
except Exception:
|
||||
self.log.exception("Exception moving Pagure event:")
|
||||
finally:
|
||||
self.connection.eventDone()
|
||||
|
||||
|
||||
class PagureAPIClient():
|
||||
log = logging.getLogger("zuul.PagureAPIClient")
|
||||
|
||||
def __init__(
|
||||
self, baseurl, api_token, project, token_exp_date=None):
|
||||
self.session = requests.Session()
|
||||
self.base_url = '%s/api/0/' % baseurl
|
||||
self.api_token = api_token
|
||||
self.project = project
|
||||
self.headers = {'Authorization': 'token %s' % self.api_token}
|
||||
self.token_exp_date = token_exp_date
|
||||
|
||||
def is_expired(self):
|
||||
if self.token_exp_date:
|
||||
if int(time.time()) > (self.token_exp_date - 3600):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, url):
|
||||
self.log.debug("Getting resource %s ..." % url)
|
||||
ret = self.session.get(url, headers=self.headers)
|
||||
self.log.debug("GET returned (code: %s): %s" % (
|
||||
ret.status_code, ret.text))
|
||||
return ret.json()
|
||||
|
||||
def post(self, url, params=None):
|
||||
self.log.info(
|
||||
"Posting on resource %s, params (%s) ..." % (url, params))
|
||||
ret = self.session.post(url, data=params, headers=self.headers)
|
||||
self.log.debug("POST returned (code: %s): %s" % (
|
||||
ret.status_code, ret.text))
|
||||
return ret.json()
|
||||
|
||||
def get_project_branches(self):
|
||||
path = '%s/git/branches' % self.project
|
||||
return self.get(self.base_url + path).get('branches', [])
|
||||
|
||||
def get_pr(self, number):
|
||||
path = '%s/pull-request/%s' % (self.project, number)
|
||||
return self.get(self.base_url + path)
|
||||
|
||||
def get_pr_diffstats(self, number):
|
||||
path = '%s/pull-request/%s/diffstats' % (self.project, number)
|
||||
return self.get(self.base_url + path)
|
||||
|
||||
def get_pr_flags(self, number, last=False):
|
||||
path = '%s/pull-request/%s/flag' % (self.project, number)
|
||||
data = self.get(self.base_url + path)
|
||||
if last:
|
||||
if data['flags']:
|
||||
return data['flags'][0]
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
return data['flags']
|
||||
|
||||
def set_pr_flag(self, number, status, url, description):
|
||||
params = {
|
||||
"username": "Zuul",
|
||||
"comment": "Jobs result is %s" % status,
|
||||
"status": status,
|
||||
"url": url}
|
||||
path = '%s/pull-request/%s/flag' % (self.project, number)
|
||||
return self.post(self.base_url + path, params)
|
||||
|
||||
def comment_pull(self, number, message):
|
||||
params = {"comment": message}
|
||||
path = '%s/pull-request/%s/comment' % (self.project, number)
|
||||
return self.post(self.base_url + path, params)
|
||||
|
||||
def merge_pr(self, number):
|
||||
path = '%s/pull-request/%s/merge' % (self.project, number)
|
||||
return self.post(self.base_url + path)
|
||||
|
||||
def create_project_api_token(self):
|
||||
""" A project admin user's api token must be use with that endpoint
|
||||
"""
|
||||
param = {
|
||||
"description": "zuul-token-%s" % int(time.time()),
|
||||
"acls": [
|
||||
"pull_request_merge", "pull_request_comment",
|
||||
"pull_request_flag"]
|
||||
}
|
||||
path = '%s/token/new' % self.project
|
||||
data = self.post(self.base_url + path, param)
|
||||
# {"token": {"description": "mytoken", "id": "IED2HC...4QIXS6WPZDTET"}}
|
||||
return data['token']
|
||||
|
||||
def get_connectors(self):
|
||||
""" A project admin user's api token must be use with that endpoint
|
||||
"""
|
||||
def get_token_epoch(token):
|
||||
return int(token['description'].split('-')[-1])
|
||||
|
||||
path = '%s/connector' % self.project
|
||||
data = self.get(self.base_url + path)
|
||||
# {"connector": {
|
||||
# "hook_token": "WCL92MLWMRPGKBQ5LI0LZCSIS4TRQMHR0Q",
|
||||
# "api_tokens": [
|
||||
# {
|
||||
# "description": "zuul-token-123",
|
||||
# "expired": false,
|
||||
# "id": "X03J4DOJT7P3G4....3DNPPXN4G144BBIAJ"
|
||||
# }
|
||||
# ]
|
||||
# }}
|
||||
# Filter expired tokens
|
||||
tokens = [
|
||||
token for token in data['connector'].get('api_tokens', {})
|
||||
if not token['expired']]
|
||||
# Now following the pattern zuul-token-{epoch} find the last
|
||||
# one created
|
||||
api_token = None
|
||||
for token in tokens:
|
||||
if not token['description'].startswith('zuul-token-'):
|
||||
continue
|
||||
epoch = get_token_epoch(token)
|
||||
if api_token:
|
||||
if epoch > get_token_epoch(api_token):
|
||||
api_token = token
|
||||
else:
|
||||
api_token = token
|
||||
if not api_token:
|
||||
# Let's create one
|
||||
api_token = self.create_project_api_token()
|
||||
api_token['created_at'] = get_token_epoch(api_token)
|
||||
webhook_token = data['connector']['hook_token']
|
||||
return api_token, webhook_token
|
||||
|
||||
|
||||
class PagureConnection(BaseConnection):
|
||||
driver_name = 'pagure'
|
||||
log = logging.getLogger("zuul.PagureConnection")
|
||||
payload_path = 'payload'
|
||||
|
||||
def __init__(self, driver, connection_name, connection_config):
|
||||
super(PagureConnection, self).__init__(
|
||||
driver, connection_name, connection_config)
|
||||
self._change_cache = {}
|
||||
self.project_branch_cache = {}
|
||||
self.projects = {}
|
||||
self.server = self.connection_config.get('server', 'pagure.io')
|
||||
self.canonical_hostname = self.connection_config.get(
|
||||
'canonical_hostname', self.server)
|
||||
self.git_ssh_key = self.connection_config.get('sshkey')
|
||||
self.admin_api_token = self.connection_config.get('api_token')
|
||||
self.baseurl = self.connection_config.get(
|
||||
'baseurl', 'https://%s' % self.server).rstrip('/')
|
||||
self.cloneurl = self.connection_config.get(
|
||||
'cloneurl', self.baseurl).rstrip('/')
|
||||
self.connectors = {}
|
||||
self.source = driver.getSource(self)
|
||||
self.event_queue = queue.Queue()
|
||||
|
||||
self.sched = None
|
||||
|
||||
def onLoad(self):
|
||||
self.log.info('Starting Pagure connection: %s' % self.connection_name)
|
||||
self.gearman_worker = PagureGearmanWorker(self)
|
||||
self.log.info('Starting event connector')
|
||||
self._start_event_connector()
|
||||
self.log.info('Starting GearmanWorker')
|
||||
self.gearman_worker.start()
|
||||
|
||||
def _start_event_connector(self):
|
||||
self.pagure_event_connector = PagureEventConnector(self)
|
||||
self.pagure_event_connector.start()
|
||||
|
||||
def _stop_event_connector(self):
|
||||
if self.pagure_event_connector:
|
||||
self.pagure_event_connector.stop()
|
||||
self.pagure_event_connector.join()
|
||||
|
||||
def onStop(self):
|
||||
if hasattr(self, 'gearman_worker'):
|
||||
self.gearman_worker.stop()
|
||||
self._stop_event_connector()
|
||||
|
||||
def addEvent(self, data, event=None):
|
||||
return self.event_queue.put((time.time(), data, event))
|
||||
|
||||
def getEvent(self):
|
||||
return self.event_queue.get()
|
||||
|
||||
def eventDone(self):
|
||||
self.event_queue.task_done()
|
||||
|
||||
def _refresh_project_connectors(self, project):
|
||||
pagure = PagureAPIClient(
|
||||
self.baseurl, self.admin_api_token, project)
|
||||
api_token, webhook_token = pagure.get_connectors()
|
||||
connector = self.connectors.setdefault(
|
||||
project, {'api_client': None, 'webhook_token': None})
|
||||
api_token_exp_date = api_token['created_at'] + 60 * 24 * 3600
|
||||
connector['api_client'] = PagureAPIClient(
|
||||
self.baseurl, api_token['id'], project,
|
||||
token_exp_date=api_token_exp_date)
|
||||
connector['webhook_token'] = webhook_token
|
||||
return connector
|
||||
|
||||
def get_project_webhook_token(self, project):
|
||||
token = self.connectors.get(
|
||||
project, {}).get('webhook_token', None)
|
||||
if token:
|
||||
self.log.debug(
|
||||
"Fetching project %s webhook_token from cache" % project)
|
||||
return token
|
||||
else:
|
||||
self.log.debug(
|
||||
"Fetching project %s webhook_token from API" % project)
|
||||
return self._refresh_project_connectors(project)['webhook_token']
|
||||
|
||||
def get_project_api_client(self, project):
|
||||
api_client = self.connectors.get(
|
||||
project, {}).get('api_client', None)
|
||||
if api_client:
|
||||
if not api_client.is_expired():
|
||||
self.log.debug(
|
||||
"Fetching project %s api_client from cache" % project)
|
||||
return api_client
|
||||
else:
|
||||
self.log.debug(
|
||||
"Project %s api token is expired (expiration date %s)" % (
|
||||
project, api_client.token_exp_date))
|
||||
self.log.debug("Building project %s api_client" % project)
|
||||
return self._refresh_project_connectors(project)['api_client']
|
||||
|
||||
def maintainCache(self, relevant):
|
||||
remove = set()
|
||||
for key, change in self._change_cache.items():
|
||||
if change not in relevant:
|
||||
remove.add(key)
|
||||
for key in remove:
|
||||
del self._change_cache[key]
|
||||
|
||||
def clearBranchCache(self):
|
||||
self.project_branch_cache = {}
|
||||
|
||||
def getWebController(self, zuul_web):
|
||||
return PagureWebController(zuul_web, self)
|
||||
|
||||
def validateWebConfig(self, config, connections):
|
||||
return True
|
||||
|
||||
def getProject(self, name):
|
||||
return self.projects.get(name)
|
||||
|
||||
def addProject(self, project):
|
||||
self.projects[project.name] = project
|
||||
|
||||
def getPullUrl(self, project, number):
|
||||
return '%s/pull-request/%s' % (self.getGitwebUrl(project), number)
|
||||
|
||||
def getGitwebUrl(self, project, sha=None):
|
||||
url = '%s/%s' % (self.baseurl, project)
|
||||
if sha is not None:
|
||||
url += '/commit/%s' % sha
|
||||
return url
|
||||
|
||||
def getProjectBranches(self, project, tenant):
|
||||
branches = self.project_branch_cache.get(project.name)
|
||||
|
||||
if branches is not None:
|
||||
return branches
|
||||
|
||||
pagure = self.get_project_api_client(project.name)
|
||||
branches = pagure.get_project_branches()
|
||||
self.project_branch_cache[project.name] = branches
|
||||
|
||||
self.log.info("Got branches for %s" % project.name)
|
||||
return branches
|
||||
|
||||
def getGitUrl(self, project):
|
||||
return '%s/%s' % (self.cloneurl, project.name)
|
||||
|
||||
def getChange(self, event, refresh=False):
|
||||
project = self.source.getProject(event.project_name)
|
||||
if event.change_number:
|
||||
self.log.info("Getting change for %s#%s" % (
|
||||
project, event.change_number))
|
||||
change = self._getChange(
|
||||
project, event.change_number, event.patch_number,
|
||||
refresh=refresh, event=event)
|
||||
change.source_event = event
|
||||
change.is_current_patchset = (change.pr.get('commit_stop') ==
|
||||
event.patch_number)
|
||||
else:
|
||||
self.log.info("Getting change for %s ref:%s" % (
|
||||
project, event.ref))
|
||||
if event.ref and event.ref.startswith('refs/tags/'):
|
||||
change = Tag(project)
|
||||
change.tag = event.ref[len('refs/tags/'):]
|
||||
elif event.ref and event.ref.startswith('refs/heads/'):
|
||||
change = Branch(project)
|
||||
change.branch = event.ref[len('refs/heads/'):]
|
||||
else:
|
||||
change = Ref(project)
|
||||
change.ref = event.ref
|
||||
change.oldrev = event.oldrev
|
||||
change.newrev = event.newrev
|
||||
change.branch = event.branch
|
||||
change.url = self.getGitwebUrl(project, sha=event.newrev)
|
||||
|
||||
# Pagure does not send files details in the git-receive event.
|
||||
# Explicitly set files to None and let the pipelines processor
|
||||
# call the merger asynchronuously
|
||||
change.files = None
|
||||
|
||||
change.source_event = event
|
||||
return change
|
||||
|
||||
def _getChange(self, project, number, patchset=None,
|
||||
refresh=False, url=None, event=None):
|
||||
key = (project.name, number, patchset)
|
||||
change = self._change_cache.get(key)
|
||||
if change and not refresh:
|
||||
self.log.debug("Getting change from cache %s" % str(key))
|
||||
return change
|
||||
if not change:
|
||||
change = PullRequest(project.name)
|
||||
change.project = project
|
||||
change.number = number
|
||||
# patchset is the tips commit of the PR
|
||||
change.patchset = patchset
|
||||
change.url = url
|
||||
change.uris = [
|
||||
'%s/%s/pull/%s' % (self.baseurl, project, number),
|
||||
]
|
||||
self._change_cache[key] = change
|
||||
try:
|
||||
self.log.debug("Getting change pr#%s from project %s" % (
|
||||
number, project.name))
|
||||
self._updateChange(change, event)
|
||||
except Exception:
|
||||
if key in self._change_cache:
|
||||
del self._change_cache[key]
|
||||
raise
|
||||
return change
|
||||
|
||||
def _hasRequiredStatusChecks(self, change):
|
||||
pagure = self.get_project_api_client(change.project.name)
|
||||
flag = pagure.get_pr_flags(change.number, last=True)
|
||||
return True if flag.get('status', '') == 'success' else False
|
||||
|
||||
def canMerge(self, change, allow_needs):
|
||||
pagure = self.get_project_api_client(change.project.name)
|
||||
pr = pagure.get_pr(change.number)
|
||||
|
||||
mergeable = False
|
||||
if pr.get('cached_merge_status') in ('FFORWARD', 'MERGE'):
|
||||
mergeable = True
|
||||
|
||||
ci_flag = False
|
||||
if self._hasRequiredStatusChecks(change):
|
||||
ci_flag = True
|
||||
|
||||
threshold = pr.get('threshold_reached')
|
||||
if threshold is None:
|
||||
self.log.debug("No threshold_reached attribute found")
|
||||
|
||||
self.log.debug(
|
||||
'PR %s#%s mergeability details mergeable: %s '
|
||||
'flag: %s threshold: %s' % (
|
||||
change.project.name, change.number, mergeable,
|
||||
ci_flag, threshold))
|
||||
|
||||
can_merge = mergeable and ci_flag and threshold
|
||||
|
||||
self.log.info('Check PR %s#%s mergeability can_merge: %s' % (
|
||||
change.project.name, change.number, can_merge))
|
||||
return can_merge
|
||||
|
||||
def getPull(self, project_name, number):
|
||||
pagure = self.get_project_api_client(project_name)
|
||||
pr = pagure.get_pr(number)
|
||||
diffstats = pagure.get_pr_diffstats(number)
|
||||
pr['files'] = diffstats.keys()
|
||||
self.log.info('Got PR %s#%s', project_name, number)
|
||||
return pr
|
||||
|
||||
def getStatus(self, project, number):
|
||||
return self.getCommitStatus(project.name, number)
|
||||
|
||||
def getScore(self, pr):
|
||||
score_board = {}
|
||||
last_pr_code_updated = 0
|
||||
# First get last PR updated date
|
||||
for comment in pr.get('comments', []):
|
||||
# PR updated are reported as comment but with the notification flag
|
||||
if comment['notification']:
|
||||
date = int(comment['date_created'])
|
||||
if date > last_pr_code_updated:
|
||||
last_pr_code_updated = date
|
||||
# Now compute the score
|
||||
# TODO(fbo): Pagure does not reset the score when a PR code is updated
|
||||
# This code block computes the score based on votes after the last PR
|
||||
# update. This should be proposed upstream
|
||||
# https://pagure.io/pagure/issue/3985
|
||||
for comment in pr.get('comments', []):
|
||||
author = comment['user']['fullname']
|
||||
date = int(comment['date_created'])
|
||||
# Only handle score since the last PR update
|
||||
if date >= last_pr_code_updated:
|
||||
score_board.setdefault(author, 0)
|
||||
# Use the same strategy to compute the score than Pagure
|
||||
if comment.get('comment', '').find(':thumbsup:') >= 0:
|
||||
score_board[author] += 1
|
||||
if comment.get('comment', '').find(':thumbsdown:') >= 0:
|
||||
score_board[author] -= 1
|
||||
return sum(score_board.values())
|
||||
|
||||
def _updateChange(self, change, event):
|
||||
self.log.info("Updating change from pagure %s" % change)
|
||||
change.pr = self.getPull(change.project.name, change.number)
|
||||
change.ref = "refs/pull/%s/head" % change.number
|
||||
change.branch = change.pr.get('branch')
|
||||
change.patchset = change.pr.get('commit_stop')
|
||||
change.files = change.pr.get('files')
|
||||
change.title = change.pr.get('title')
|
||||
change.open = change.pr.get('status') == 'Open'
|
||||
change.is_merged = change.pr.get('status') == 'Merged'
|
||||
change.status = self.getStatus(change.project, change.number)
|
||||
change.score = self.getScore(change.pr)
|
||||
change.message = change.pr.get('initial_comment') or ''
|
||||
# last_updated seems to be touch for comment changed/flags - that's OK
|
||||
change.updated_at = change.pr.get('last_updated')
|
||||
self.log.info("Updated change from pagure %s" % change)
|
||||
|
||||
if self.sched:
|
||||
self.sched.onChangeUpdated(change, event)
|
||||
|
||||
return change
|
||||
|
||||
def commentPull(self, project, number, message):
|
||||
pagure = self.get_project_api_client(project)
|
||||
pagure.comment_pull(number, message)
|
||||
self.log.info("Commented on PR %s#%s", project, number)
|
||||
|
||||
def setCommitStatus(self, project, number, state, url='',
|
||||
description='', context=''):
|
||||
pagure = self.get_project_api_client(project)
|
||||
pagure.set_pr_flag(number, state, url, description)
|
||||
self.log.info("Set pull-request CI flag status : %s" % description)
|
||||
# Wait for 1 second as flag timestamp is by second
|
||||
time.sleep(1)
|
||||
|
||||
def getCommitStatus(self, project, number):
|
||||
pagure = self.get_project_api_client(project)
|
||||
flag = pagure.get_pr_flags(number, last=True)
|
||||
self.log.info(
|
||||
"Got pull-request CI status for PR %s on %s status: %s" % (
|
||||
number, project, flag.get('status')))
|
||||
return flag.get('status')
|
||||
|
||||
def getChangesDependingOn(self, change, projects, tenant):
|
||||
""" Reverse lookup of PR depending on this one
|
||||
"""
|
||||
# TODO(fbo) No way to Query pagure to search accross projects' PRs for
|
||||
# a the depends-on string in PR initial message. Not a blocker
|
||||
# for now, let's workaround using the local change cache !
|
||||
changes_dependencies = []
|
||||
for cached_change_id, _change in self._change_cache.items():
|
||||
for dep_header in dependson.find_dependency_headers(
|
||||
_change.message):
|
||||
if change.url in dep_header:
|
||||
changes_dependencies.append(_change)
|
||||
return changes_dependencies
|
||||
|
||||
def mergePull(self, project, number):
|
||||
pagure = self.get_project_api_client(project)
|
||||
pagure.merge_pr(number)
|
||||
self.log.debug("Merged PR %s#%s", project, number)
|
||||
|
||||
|
||||
class PagureWebController(BaseWebController):
|
||||
|
||||
log = logging.getLogger("zuul.PagureWebController")
|
||||
|
||||
def __init__(self, zuul_web, connection):
|
||||
self.connection = connection
|
||||
self.zuul_web = zuul_web
|
||||
|
||||
def _validate_signature(self, body, headers):
|
||||
try:
|
||||
request_signature = headers['x-pagure-signature']
|
||||
except KeyError:
|
||||
raise cherrypy.HTTPError(401, 'x-pagure-signature header missing.')
|
||||
|
||||
project = headers['x-pagure-project']
|
||||
token = self.connection.get_project_webhook_token(project)
|
||||
if not token:
|
||||
raise cherrypy.HTTPError(
|
||||
401, 'no webhook token for %s.' % project)
|
||||
|
||||
signature, payload = _sign_request(body, token)
|
||||
|
||||
if not hmac.compare_digest(str(signature), str(request_signature)):
|
||||
self.log.debug(
|
||||
"Missmatch (Payload Signature: %s, Request Signature: %s)" % (
|
||||
signature, request_signature))
|
||||
raise cherrypy.HTTPError(
|
||||
401,
|
||||
'Request signature does not match calculated payload '
|
||||
'signature. Check that secret is correct.')
|
||||
|
||||
return payload
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
def payload(self):
|
||||
# https://docs.pagure.org/pagure/usage/using_webhooks.html
|
||||
headers = dict()
|
||||
for key, value in cherrypy.request.headers.items():
|
||||
headers[key.lower()] = value
|
||||
body = cherrypy.request.body.read()
|
||||
payload = self._validate_signature(body, headers)
|
||||
json_payload = json.loads(payload.decode('utf-8'))
|
||||
|
||||
job = self.zuul_web.rpc.submitJob(
|
||||
'pagure:%s:payload' % self.connection.connection_name,
|
||||
{'payload': json_payload})
|
||||
|
||||
return json.loads(job.data[0])
|
||||
|
||||
|
||||
def getSchema():
|
||||
pagure_connection = v.Any(str, v.Schema(dict))
|
||||
return pagure_connection
|
|
@ -0,0 +1,206 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
from zuul.model import Change, TriggerEvent, EventFilter, RefFilter
|
||||
|
||||
EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
|
||||
|
||||
|
||||
class PullRequest(Change):
|
||||
def __init__(self, project):
|
||||
super(PullRequest, self).__init__(project)
|
||||
self.project = None
|
||||
self.pr = None
|
||||
self.updated_at = None
|
||||
self.title = None
|
||||
self.score = 0
|
||||
self.files = []
|
||||
|
||||
def __repr__(self):
|
||||
r = ['<Change 0x%x' % id(self)]
|
||||
if self.project:
|
||||
r.append('project: %s' % self.project)
|
||||
if self.number:
|
||||
r.append('number: %s' % self.number)
|
||||
if self.patchset:
|
||||
r.append('patchset: %s' % self.patchset)
|
||||
if self.updated_at:
|
||||
r.append('updated: %s' % self.updated_at)
|
||||
if self.status:
|
||||
r.append('status: %s' % self.status)
|
||||
if self.score:
|
||||
r.append('score: %s' % self.score)
|
||||
if self.is_merged:
|
||||
r.append('state: merged')
|
||||
if self.open:
|
||||
r.append('state: open')
|
||||
return ' '.join(r) + '>'
|
||||
|
||||
def isUpdateOf(self, other):
|
||||
if (self.project == other.project and
|
||||
hasattr(other, 'number') and self.number == other.number and
|
||||
hasattr(other, 'updated_at') and
|
||||
self.updated_at > other.updated_at):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PagureTriggerEvent(TriggerEvent):
|
||||
def __init__(self):
|
||||
super(PagureTriggerEvent, self).__init__()
|
||||
self.trigger_name = 'pagure'
|
||||
self.title = None
|
||||
self.action = None
|
||||
self.status = None
|
||||
|
||||
def _repr(self):
|
||||
r = [super(PagureTriggerEvent, self)._repr()]
|
||||
if self.action:
|
||||
r.append("action:%s" % self.action)
|
||||
if self.status:
|
||||
r.append("status:%s" % self.status)
|
||||
r.append("project:%s" % self.canonical_project_name)
|
||||
if self.change_number:
|
||||
r.append("pr:%s" % self.change_number)
|
||||
return ' '.join(r)
|
||||
|
||||
def isPatchsetCreated(self):
|
||||
if self.type == 'pg_pull_request':
|
||||
return self.action in ['opened', 'changed']
|
||||
return False
|
||||
|
||||
|
||||
class PagureEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=[], refs=[], statuses=[],
|
||||
comments=[], actions=[], ignore_deletes=True):
|
||||
|
||||
EventFilter.__init__(self, trigger)
|
||||
|
||||
self._types = types
|
||||
self._refs = refs
|
||||
self._comments = comments
|
||||
self.types = [re.compile(x) for x in types]
|
||||
self.refs = [re.compile(x) for x in refs]
|
||||
self.comments = [re.compile(x) for x in comments]
|
||||
self.actions = actions
|
||||
self.statuses = statuses
|
||||
self.ignore_deletes = ignore_deletes
|
||||
|
||||
def __repr__(self):
|
||||
ret = '<PagureEventFilter'
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
if self._refs:
|
||||
ret += ' refs: %s' % ', '.join(self._refs)
|
||||
if self.ignore_deletes:
|
||||
ret += ' ignore_deletes: %s' % self.ignore_deletes
|
||||
if self._comments:
|
||||
ret += ' comments: %s' % ', '.join(self._comments)
|
||||
if self.actions:
|
||||
ret += ' actions: %s' % ', '.join(self.actions)
|
||||
if self.statuses:
|
||||
ret += ' statuses: %s' % ', '.join(self.statuses)
|
||||
ret += '>'
|
||||
|
||||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
if etype.match(event.type):
|
||||
matches_type = True
|
||||
if self.types and not matches_type:
|
||||
return False
|
||||
|
||||
matches_ref = False
|
||||
if event.ref is not None:
|
||||
for ref in self.refs:
|
||||
if ref.match(event.ref):
|
||||
matches_ref = True
|
||||
if self.refs and not matches_ref:
|
||||
return False
|
||||
if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
|
||||
# If the updated ref has an empty git sha (all 0s),
|
||||
# then the ref is being deleted
|
||||
return False
|
||||
|
||||
matches_comment_re = False
|
||||
for comment_re in self.comments:
|
||||
if (event.comment is not None and
|
||||
comment_re.search(event.comment)):
|
||||
matches_comment_re = True
|
||||
if self.comments and not matches_comment_re:
|
||||
return False
|
||||
|
||||
matches_action = False
|
||||
for action in self.actions:
|
||||
if (event.action == action):
|
||||
matches_action = True
|
||||
if self.actions and not matches_action:
|
||||
return False
|
||||
|
||||
matches_status = False
|
||||
for status in self.statuses:
|
||||
if event.status == status:
|
||||
matches_status = True
|
||||
if self.statuses and not matches_status:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# The RefFilter should be understood as RequireFilter (it maps to
|
||||
# pipeline requires definition)
|
||||
class PagureRefFilter(RefFilter):
|
||||
def __init__(self, connection_name, score=None,
|
||||
open=None, merged=None, status=None):
|
||||
RefFilter.__init__(self, connection_name)
|
||||
self.score = score
|
||||
self.open = open
|
||||
self.merged = merged
|
||||
self.status = status
|
||||
|
||||
def __repr__(self):
|
||||
ret = '<PagureRefFilter connection_name: %s ' % self.connection_name
|
||||
if self.score:
|
||||
ret += ' score: %s' % self.score
|
||||
if self.open is not None:
|
||||
ret += ' open: %s' % self.open
|
||||
if self.merged is not None:
|
||||
ret += ' merged: %s' % self.merged
|
||||
if self.status is not None:
|
||||
ret += ' status: %s' % self.status
|
||||
ret += '>'
|
||||
return ret
|
||||
|
||||
def matches(self, change):
|
||||
if self.score is not None:
|
||||
if change.score < self.score:
|
||||
return False
|
||||
|
||||
if self.open is not None:
|
||||
if change.open != self.open:
|
||||
return False
|
||||
|
||||
if self.merged is not None:
|
||||
if change.is_merged != self.merged:
|
||||
return False
|
||||
|
||||
if self.status is not None:
|
||||
if change.status != self.status:
|
||||
return False
|
||||
|
||||
return True
|
|
@ -0,0 +1,141 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import time
|
||||
import logging
|
||||
import voluptuous as v
|
||||
|
||||
from zuul.reporter import BaseReporter
|
||||
from zuul.exceptions import MergeFailure
|
||||
from zuul.driver.pagure.paguresource import PagureSource
|
||||
|
||||
|
||||
class PagureReporter(BaseReporter):
|
||||
"""Sends off reports to Pagure."""
|
||||
|
||||
name = 'pagure'
|
||||
log = logging.getLogger("zuul.PagureReporter")
|
||||
|
||||
def __init__(self, driver, connection, pipeline, config=None):
|
||||
super(PagureReporter, self).__init__(driver, connection, config)
|
||||
self._commit_status = self.config.get('status', None)
|
||||
self._create_comment = self.config.get('comment', True)
|
||||
self._merge = self.config.get('merge', False)
|
||||
self.context = "{}/{}".format(pipeline.tenant.name, pipeline.name)
|
||||
|
||||
def report(self, item):
|
||||
"""Report on an event."""
|
||||
|
||||
# If the source is not PagureSource we cannot report anything here.
|
||||
if not isinstance(item.change.project.source, PagureSource):
|
||||
return
|
||||
|
||||
# For supporting several Pagure connections we also must filter by
|
||||
# the canonical hostname.
|
||||
if item.change.project.source.connection.canonical_hostname != \
|
||||
self.connection.canonical_hostname:
|
||||
return
|
||||
|
||||
if self._commit_status is not None:
|
||||
if (hasattr(item.change, 'patchset') and
|
||||
item.change.patchset is not None):
|
||||
self.setCommitStatus(item)
|
||||
elif (hasattr(item.change, 'newrev') and
|
||||
item.change.newrev is not None):
|
||||
self.setCommitStatus(item)
|
||||
if hasattr(item.change, 'number'):
|
||||
if self._create_comment:
|
||||
self.addPullComment(item)
|
||||
if self._merge:
|
||||
self.mergePull(item)
|
||||
if not item.change.is_merged:
|
||||
msg = self._formatItemReportMergeFailure(item)
|
||||
self.addPullComment(item, msg)
|
||||
|
||||
def _formatItemReportJobs(self, item):
|
||||
# Return the list of jobs portion of the report
|
||||
ret = ''
|
||||
jobs_fields = self._getItemReportJobsFields(item)
|
||||
for job_fields in jobs_fields:
|
||||
ret += '- [%s](%s) : %s%s%s%s\n' % job_fields
|
||||
return ret
|
||||
|
||||
def addPullComment(self, item, comment=None):
|
||||
message = comment or self._formatItemReport(item)
|
||||
project = item.change.project.name
|
||||
pr_number = item.change.number
|
||||
self.log.debug(
|
||||
'Reporting change %s, params %s, message: %s' %
|
||||
(item.change, self.config, message))
|
||||
self.connection.commentPull(project, pr_number, message)
|
||||
|
||||
def setCommitStatus(self, item):
|
||||
project = item.change.project.name
|
||||
if hasattr(item.change, 'patchset'):
|
||||
sha = item.change.patchset
|
||||
elif hasattr(item.change, 'newrev'):
|
||||
sha = item.change.newrev
|
||||
state = self._commit_status
|
||||
change_number = item.change.number
|
||||
|
||||
url_pattern = self.config.get('status-url')
|
||||
sched_config = self.connection.sched.config
|
||||
if sched_config.has_option('web', 'status_url'):
|
||||
url_pattern = sched_config.get('web', 'status_url')
|
||||
url = item.formatUrlPattern(url_pattern) \
|
||||
if url_pattern else 'https://sftests.com'
|
||||
|
||||
description = '%s status: %s (%s)' % (
|
||||
item.pipeline.name, self._commit_status, sha)
|
||||
|
||||
self.log.debug(
|
||||
'Reporting change %s, params %s, '
|
||||
'context: %s, state: %s, description: %s, url: %s' %
|
||||
(item.change, self.config,
|
||||
self.context, state, description, url))
|
||||
|
||||
self.connection.setCommitStatus(
|
||||
project, change_number, state, url, description, self.context)
|
||||
|
||||
def mergePull(self, item):
|
||||
project = item.change.project.name
|
||||
pr_number = item.change.number
|
||||
|
||||
for i in [1, 2]:
|
||||
try:
|
||||
self.connection.mergePull(project, pr_number)
|
||||
item.change.is_merged = True
|
||||
return
|
||||
except MergeFailure:
|
||||
self.log.exception(
|
||||
'Merge attempt of change %s %s/2 failed.' %
|
||||
(item.change, i), exc_info=True)
|
||||
if i == 1:
|
||||
time.sleep(2)
|
||||
self.log.warning(
|
||||
'Merge of change %s failed after 2 attempts, giving up' %
|
||||
item.change)
|
||||
|
||||
def getSubmitAllowNeeds(self):
|
||||
return []
|
||||
|
||||
|
||||
def getSchema():
|
||||
pagure_reporter = v.Schema({
|
||||
'status': v.Any('pending', 'success', 'failure'),
|
||||
'status-url': str,
|
||||
'comment': bool,
|
||||
'merge': bool,
|
||||
})
|
||||
return pagure_reporter
|
|
@ -0,0 +1,151 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import logging
|
||||
|
||||
from zuul.source import BaseSource
|
||||
from zuul.model import Project
|
||||
|
||||
from zuul.driver.pagure.paguremodel import PagureRefFilter
|
||||
|
||||
|
||||
class PagureSource(BaseSource):
|
||||
name = 'pagure'
|
||||
log = logging.getLogger("zuul.source.PagureSource")
|
||||
|
||||
def __init__(self, driver, connection, config=None):
|
||||
hostname = connection.canonical_hostname
|
||||
super(PagureSource, self).__init__(driver, connection,
|
||||
hostname, config)
|
||||
self.change_re = re.compile(r"/(.*?)/pull-request/(\d+)")
|
||||
|
||||
def getRefSha(self, project, ref):
|
||||
"""Return a sha for a given project ref."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def waitForRefSha(self, project, ref, old_sha=''):
|
||||
"""Block until a ref shows up in a given project."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def isMerged(self, change, head=None):
|
||||
"""Determine if change is merged."""
|
||||
if not change.number:
|
||||
# Not a pull request, considering merged.
|
||||
return True
|
||||
return change.is_merged
|
||||
|
||||
def canMerge(self, change, allow_needs):
|
||||
"""Determine if change can merge."""
|
||||
if not change.number:
|
||||
# Not a pull request, considering merged.
|
||||
return True
|
||||
return self.connection.canMerge(change, allow_needs)
|
||||
|
||||
def postConfig(self):
|
||||
"""Called after configuration has been processed."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def getChange(self, event, refresh=False):
|
||||
return self.connection.getChange(event, refresh)
|
||||
|
||||
def getChangeByURL(self, url):
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
except ValueError:
|
||||
return None
|
||||
m = self.change_re.match(parsed.path)
|
||||
if not m:
|
||||
return None
|
||||
project_name = m.group(1)
|
||||
try:
|
||||
num = int(m.group(2))
|
||||
except ValueError:
|
||||
return None
|
||||
pull = self.connection.getPull(project_name, num)
|
||||
if not pull:
|
||||
return None
|
||||
project = self.getProject(project_name)
|
||||
change = self.connection._getChange(
|
||||
project, num,
|
||||
patchset=pull.get('commit_stop'),
|
||||
url=url)
|
||||
return change
|
||||
|
||||
def getChangesDependingOn(self, change, projects, tenant):
|
||||
return self.connection.getChangesDependingOn(
|
||||
change, projects, tenant)
|
||||
|
||||
def getCachedChanges(self):
|
||||
return self.connection._change_cache.values()
|
||||
|
||||
def getProject(self, name):
|
||||
p = self.connection.getProject(name)
|
||||
if not p:
|
||||
p = Project(name, self)
|
||||
self.connection.addProject(p)
|
||||
return p
|
||||
|
||||
def getProjectBranches(self, project, tenant):
|
||||
return self.connection.getProjectBranches(project, tenant)
|
||||
|
||||
def getProjectOpenChanges(self, project):
|
||||
"""Get the open changes for a project."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def updateChange(self, change, history=None):
|
||||
"""Update information for a change."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def getGitUrl(self, project):
|
||||
"""Get the git url for a project."""
|
||||
return self.connection.getGitUrl(project)
|
||||
|
||||
def getGitwebUrl(self, project, sha=None):
|
||||
"""Get the git-web url for a project."""
|
||||
raise NotImplementedError()
|
||||
|
||||
# This driver does not implement pipeline requirements.
|
||||
def getRequireFilters(self, config):
|
||||
f = PagureRefFilter(
|
||||
connection_name=self.connection.connection_name,
|
||||
score=config.get('score'),
|
||||
open=config.get('open'),
|
||||
merged=config.get('merged'),
|
||||
status=config.get('status'),
|
||||
)
|
||||
return [f]
|
||||
|
||||
def getRejectFilters(self, config):
|
||||
raise NotImplementedError()
|
||||
|
||||
def getRefForChange(self, change):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# Require model
|
||||
def getRequireSchema():
|
||||
require = {
|
||||
'score': int,
|
||||
'open': bool,
|
||||
'merged': bool,
|
||||
'status': str,
|
||||
}
|
||||
return require
|
||||
|
||||
|
||||
def getRejectSchema():
|
||||
reject = {}
|
||||
return reject
|
|
@ -0,0 +1,61 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import voluptuous as v
|
||||
from zuul.trigger import BaseTrigger
|
||||
from zuul.driver.pagure.paguremodel import PagureEventFilter
|
||||
from zuul.driver.util import scalar_or_list, to_list
|
||||
|
||||
|
||||
class PagureTrigger(BaseTrigger):
|
||||
name = 'pagure'
|
||||
log = logging.getLogger("zuul.trigger.PagureTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_config):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_config):
|
||||
f = PagureEventFilter(
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
actions=to_list(trigger.get('action')),
|
||||
refs=to_list(trigger.get('ref')),
|
||||
comments=to_list(trigger.get('comment')),
|
||||
statuses=to_list(trigger.get('status')),
|
||||
)
|
||||
efilters.append(f)
|
||||
|
||||
return efilters
|
||||
|
||||
def onPullRequest(self, payload):
|
||||
pass
|
||||
|
||||
|
||||
def getSchema():
|
||||
pagure_trigger = {
|
||||
v.Required('event'):
|
||||
# Cannot use same event type than github as it collapse
|
||||
# with Registered github triggers if any. The Event filter
|
||||
# does not have the connections info like the Ref filter (require)
|
||||
# have. See manager/__init__.py:addChange
|
||||
scalar_or_list(v.Any('pg_pull_request',
|
||||
'pg_pull_request_review',
|
||||
'pg_push')),
|
||||
'action': scalar_or_list(str),
|
||||
'ref': scalar_or_list(str),
|
||||
'comment': scalar_or_list(str),
|
||||
'status': scalar_or_list(str),
|
||||
}
|
||||
|
||||
return pagure_trigger
|
|
@ -27,6 +27,7 @@ import zuul.driver.sql
|
|||
import zuul.driver.bubblewrap
|
||||
import zuul.driver.nullwrap
|
||||
import zuul.driver.mqtt
|
||||
import zuul.driver.pagure
|
||||
from zuul.connection import BaseConnection
|
||||
from zuul.driver import SourceInterface
|
||||
|
||||
|
@ -54,6 +55,7 @@ class ConnectionRegistry(object):
|
|||
self.registerDriver(zuul.driver.bubblewrap.BubblewrapDriver())
|
||||
self.registerDriver(zuul.driver.nullwrap.NullwrapDriver())
|
||||
self.registerDriver(zuul.driver.mqtt.MQTTDriver())
|
||||
self.registerDriver(zuul.driver.pagure.PagureDriver())
|
||||
|
||||
def registerDriver(self, driver):
|
||||
if driver.name in self.drivers:
|
||||
|
|
Loading…
Reference in New Issue