Merge "Gear: bind to a specified address"
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 6c77477..6d3b44a 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -462,13 +462,21 @@
A deprecated alternate spelling of *comment*. Only one of *comment* or
*comment_filter* should be used.
- *require-approval*
+ *require-any-approval*
This may be used for any event. It requires that a certain kind
of approval be present for the current patchset of the change (the
approval could be added by the event in question). It follows the
same syntax as the :ref:`"approval" pipeline requirement below
<pipeline-require-approval>`.
+ *require-all-approvals*
+ This takes a list of approvals in the same format as
+ *require-any-approval* but requires all approvals match the rules.
+
+ **require-approval** (depreciated)
+ A deprecated alternate spelling of *require-any-approval*. This will
+ be joined with *require-any-approval* if both are present.
+
**timer**
This trigger will run based on a cron-style time specification.
It will enqueue an event into its pipeline for every project
@@ -515,7 +523,7 @@
.. _pipeline-require-approval:
- **approval**
+ **any-approval**
This requires that a certain kind of approval be present for the
current patchset of the change (the approval could be added by the
event in question). It takes several sub-parameters, all of which
@@ -549,6 +557,24 @@
be a single value or a list: ``verified: [1, 2]`` would match
either a +1 or +2 vote.
+ You can also match negative conditions by starting with an
+ exclamation mark (!). This requires the value to be a string.
+ Example: ``verified: '![-1, -2]'``
+
+ This takes a list of approvals in the same format as above. It
+ requires that any approval on a change can meet the specified
+ criteria.
+
+ **all-approvals**
+ This takes a list of approvals in the same format as *any-approval* but
+ requires all approvals match the rules. For example, you can stop any
+ new changes from queueing when there is a negative vote by requiring
+ all approves to not have a -1.
+
+ **approval** (depreciated)
+ A deprecated alternate spelling of *any-approval*. This will be
+ joined with *any-approval* if both are present.
+
**open**
A boolean value (``true`` or ``false``) that indicates whether the change
must be open or closed in order to be enqueued.
@@ -635,7 +661,7 @@
Default: ``linear``.
**window-increase-factor**
- DependentPipelineManagers only. The value to be added or mulitplied
+ DependentPipelineManagers only. The value to be added or multiplied
against the previous window value to determine the new window after
successful change merges.
Default: ``1``.
diff --git a/etc/status/.jshintignore b/etc/status/.jshintignore
new file mode 100644
index 0000000..218f297
--- /dev/null
+++ b/etc/status/.jshintignore
@@ -0,0 +1 @@
+public_html/lib
diff --git a/etc/status/.jshintrc b/etc/status/.jshintrc
new file mode 100644
index 0000000..15bd571
--- /dev/null
+++ b/etc/status/.jshintrc
@@ -0,0 +1,21 @@
+{
+ "bitwise": true,
+ "eqeqeq": true,
+ "forin": true,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "noempty": true,
+ "nonew": true,
+ "undef": true,
+ "unused": true,
+
+ "strict": false,
+ "laxbreak": true,
+ "browser": true,
+
+ "predef": [
+ "jQuery",
+ "zuul"
+ ]
+}
diff --git a/etc/status/public_html/jquery.zuul.js b/etc/status/public_html/jquery.zuul.js
index 40a5d4d..1db3c8e 100644
--- a/etc/status/public_html/jquery.zuul.js
+++ b/etc/status/public_html/jquery.zuul.js
@@ -16,9 +16,10 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
-'use strict';
(function ($) {
+ 'use strict';
+
function set_cookie(name, value) {
document.cookie = name + '=' + value + '; path=/';
}
@@ -39,7 +40,7 @@
}
$.zuul = function(options) {
- var options = $.extend({
+ options = $.extend({
'enabled': true,
'graphite_url': '',
'source': 'status.json',
@@ -72,7 +73,7 @@
hideGrid: true,
target: [
"color(stats.gauges.zuul.pipeline." + pipeline_name
- + ".current_changes, '6b8182')"
+ + ".current_changes, '6b8182')"
]
});
}
@@ -616,7 +617,7 @@
var app = {
schedule: function (app) {
- var app = app || this;
+ app = app || this;
if (!options.enabled) {
setTimeout(function() {app.schedule(app);}, 5000);
return;
@@ -642,7 +643,7 @@
this.emit('update-start');
var app = this;
- var $msg = $(options.msg_id)
+ var $msg = $(options.msg_id);
xhr = $.getJSON(options.source)
.done(function (data) {
if ('message' in data) {
@@ -682,7 +683,10 @@
data.result_event_queue.length : '0'
);
})
- .fail(function (err, jqXHR, errMsg) {
+ .fail(function (jqXHR, statusText, errMsg) {
+ if (statusText === 'abort') {
+ return;
+ }
$msg.text(options.source + ': ' + errMsg)
.addClass('alert-danger')
.removeClass('zuul-msg-wrap-off')
@@ -701,7 +705,7 @@
var newimg = new Image();
var parts = url.split('#');
newimg.src = parts[0] + '#' + new Date().getTime();
- $(newimg).load(function (x) {
+ $(newimg).load(function () {
zuul_sparkline_urls[name] = newimg.src;
});
});
@@ -897,5 +901,5 @@
app: app,
jq: $jq
};
- }
+ };
}(jQuery));
diff --git a/etc/status/public_html/zuul.app.js b/etc/status/public_html/zuul.app.js
index 640437b..6321af8 100644
--- a/etc/status/public_html/zuul.app.js
+++ b/etc/status/public_html/zuul.app.js
@@ -17,9 +17,11 @@
// License for the specific language governing permissions and limitations
// under the License.
+/*exported zuul_build_dom, zuul_start */
+
function zuul_build_dom($, container) {
// Build a default-looking DOM
- default_layout = '<div class="container">'
+ var default_layout = '<div class="container">'
+ '<h1>Zuul Status</h1>'
+ '<p>Real-time status monitor of Zuul, the pipeline manager between Gerrit and Workers.</p>'
+ '<div class="zuul-container" id="zuul-container">'
@@ -34,7 +36,7 @@
$(function ($) {
// DOM ready
- $container = $(container);
+ var $container = $(container);
$container.html(default_layout);
});
}
diff --git a/tests/base.py b/tests/base.py
index 8c96d18..5ddb160 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -47,8 +47,9 @@
import zuul.rpclistener
import zuul.launcher.gearman
import zuul.lib.swift
-import zuul.merger.server
import zuul.merger.client
+import zuul.merger.merger
+import zuul.merger.server
import zuul.reporter.gerrit
import zuul.reporter.smtp
import zuul.trigger.gerrit
@@ -145,7 +146,7 @@
self.latest_patchset),
'refs/tags/init')
repo.head.reference = ref
- repo.head.reset(index=True, working_tree=True)
+ zuul.merger.merger.reset_repo_to_head(repo)
repo.git.clean('-x', '-f', '-d')
path = os.path.join(self.upstream_root, self.project)
@@ -167,7 +168,7 @@
r = repo.index.commit(msg)
repo.head.reference = 'master'
- repo.head.reset(index=True, working_tree=True)
+ zuul.merger.merger.reset_repo_to_head(repo)
repo.git.clean('-x', '-f', '-d')
repo.heads['master'].checkout()
return r
@@ -258,8 +259,8 @@
"comment": "This is a comment"}
return event
- def addApproval(self, category, value, username='jenkins',
- granted_on=None):
+ def addApproval(self, category, value, username='reviewer_john',
+ granted_on=None, message=''):
if not granted_on:
granted_on = time.time()
approval = {
@@ -277,20 +278,20 @@
del self.patchsets[-1]['approvals'][i]
self.patchsets[-1]['approvals'].append(approval)
event = {'approvals': [approval],
- 'author': {'email': 'user@example.com',
- 'name': 'User Name',
- 'username': 'username'},
+ 'author': {'email': 'author@example.com',
+ 'name': 'Patchset Author',
+ 'username': 'author_phil'},
'change': {'branch': self.branch,
'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
'number': str(self.number),
- 'owner': {'email': 'user@example.com',
- 'name': 'User Name',
- 'username': 'username'},
+ 'owner': {'email': 'owner@example.com',
+ 'name': 'Change Owner',
+ 'username': 'owner_jane'},
'project': self.project,
'subject': self.subject,
'topic': 'master',
'url': 'https://hostname/459'},
- 'comment': '',
+ 'comment': message,
'patchSet': self.patchsets[-1],
'type': 'comment-added'}
self.data['submitRecords'] = self.getSubmitRecords()
@@ -380,11 +381,16 @@
class FakeGerrit(object):
log = logging.getLogger("zuul.test.FakeGerrit")
- def __init__(self, *args, **kw):
+ def __init__(self, hostname, username, port=29418, keyfile=None,
+ changes_dbs={}):
+ self.hostname = hostname
+ self.username = username
+ self.port = port
+ self.keyfile = keyfile
self.event_queue = Queue.Queue()
self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
self.change_number = 0
- self.changes = {}
+ self.changes = changes_dbs.get(hostname, {})
self.queries = []
def addFakeChange(self, project, branch, subject, status='NEW'):
@@ -407,7 +413,27 @@
def review(self, project, changeid, message, action):
number, ps = changeid.split(',')
change = self.changes[int(number)]
+
+ # Add the approval back onto the change (ie simulate what gerrit would
+ # do).
+ # Usually when zuul leaves a review it'll create a feedback loop where
+ # zuul's review enters another gerrit event (which is then picked up by
+ # zuul). However, we can't mimic this behaviour (by adding this
+ # approval event into the queue) as it stops jobs from checking what
+ # happens before this event is triggered. If a job needs to see what
+ # happens they can add their own verified event into the queue.
+ # Nevertheless, we can update change with the new review in gerrit.
+
+ for cat in ['CRVW', 'VRFY', 'APRV']:
+ if cat in action:
+ change.addApproval(cat, action[cat], username=self.username)
+
+ if 'label' in action:
+ parts = action['label'].split('=')
+ change.addApproval(parts[0], parts[2], username=self.username)
+
change.messages.append(message)
+
if 'submit' in action:
change.setMerged()
if message:
@@ -892,6 +918,7 @@
self.init_repo("org/conflict-project")
self.init_repo("org/noop-project")
self.init_repo("org/experimental-project")
+ self.init_repo("org/no-jobs-project")
self.statsd = FakeStatsd()
os.environ['STATSD_HOST'] = 'localhost'
@@ -938,7 +965,19 @@
args = [self.smtp_messages] + list(args)
return FakeSMTP(*args, **kw)
- zuul.lib.gerrit.Gerrit = FakeGerrit
+ # Set a changes database so multiple FakeGerrit's can report back to
+ # a virtual canonical database given by the configured hostname
+ self.gerrit_changes_dbs = {
+ self.config.get('gerrit', 'server'): {}
+ }
+
+ def FakeGerritFactory(*args, **kw):
+ kw['changes_dbs'] = self.gerrit_changes_dbs
+ return FakeGerrit(*args, **kw)
+
+ self.useFixture(fixtures.MonkeyPatch('zuul.lib.gerrit.Gerrit',
+ FakeGerritFactory))
+
self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
self.gerrit = FakeGerritTrigger(
@@ -1044,7 +1083,7 @@
repo.create_tag('init')
repo.head.reference = master
- repo.head.reset(index=True, working_tree=True)
+ zuul.merger.merger.reset_repo_to_head(repo)
repo.git.clean('-x', '-f', '-d')
self.create_branch(project, 'mp')
@@ -1063,7 +1102,7 @@
repo.index.commit('%s commit' % branch)
repo.head.reference = repo.heads['master']
- repo.head.reset(index=True, working_tree=True)
+ zuul.merger.merger.reset_repo_to_head(repo)
repo.git.clean('-x', '-f', '-d')
def ref_has_change(self, ref, change):
diff --git a/tests/fixtures/layout-live-reconfiguration-add-job.yaml b/tests/fixtures/layout-live-reconfiguration-add-job.yaml
new file mode 100644
index 0000000..e4aea6f
--- /dev/null
+++ b/tests/fixtures/layout-live-reconfiguration-add-job.yaml
@@ -0,0 +1,38 @@
+pipelines:
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+jobs:
+ - name: ^.*-merge$
+ failure-message: Unable to merge change
+ hold-following-changes: true
+ - name: project-testfile
+ files:
+ - '.*-requires'
+
+projects:
+ - name: org/project
+ merge-mode: cherry-pick
+ gate:
+ - project-merge:
+ - project-test1
+ - project-test2
+ - project-test3
+ - project-testfile
diff --git a/tests/fixtures/layout-requirement-all.yaml b/tests/fixtures/layout-requirement-all.yaml
new file mode 100644
index 0000000..968739d
--- /dev/null
+++ b/tests/fixtures/layout-requirement-all.yaml
@@ -0,0 +1,41 @@
+pipelines:
+ - name: pipeline
+ manager: IndependentPipelineManager
+ require:
+ all-approvals:
+ - username: jenkins
+ verified: [1, 2]
+ - verified: "![-1, -2]"
+ trigger:
+ gerrit:
+ - event: comment-added
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: trigger
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: comment-added
+ require-all-approvals:
+ - username: jenkins
+ verified: [1, 2]
+ - verified: "![-1, -2]"
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+projects:
+ - name: org/project1
+ pipeline:
+ - project1-pipeline
+ - name: org/project2
+ trigger:
+ - project2-trigger
diff --git a/tests/fixtures/layout-requirement-any.yaml b/tests/fixtures/layout-requirement-any.yaml
new file mode 100644
index 0000000..6275d8d
--- /dev/null
+++ b/tests/fixtures/layout-requirement-any.yaml
@@ -0,0 +1,43 @@
+pipelines:
+ - name: pipeline
+ manager: IndependentPipelineManager
+ require:
+ any-approval:
+ - username: jenkins
+ verified: [1, 2]
+ - username: core-reviewer
+ code-review: "![-1, -2]"
+ trigger:
+ gerrit:
+ - event: comment-added
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: trigger
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: comment-added
+ require-any-approval:
+ - username: jenkins
+ verified: [1, 2]
+ - username: core-reviewer
+ code-review: "![-1, -2]"
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+projects:
+ - name: org/project1
+ pipeline:
+ - project1-pipeline
+ - name: org/project2
+ trigger:
+ - project2-trigger
diff --git a/tests/fixtures/layout-requirement-email.yaml b/tests/fixtures/layout-requirement-email.yaml
index 4bfb733..dadcd6c 100644
--- a/tests/fixtures/layout-requirement-email.yaml
+++ b/tests/fixtures/layout-requirement-email.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- email: jenkins@example.com
trigger:
gerrit:
@@ -19,7 +19,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- email: jenkins@example.com
success:
gerrit:
diff --git a/tests/fixtures/layout-requirement-negative-username.yaml b/tests/fixtures/layout-requirement-negative-username.yaml
new file mode 100644
index 0000000..f542b86
--- /dev/null
+++ b/tests/fixtures/layout-requirement-negative-username.yaml
@@ -0,0 +1,37 @@
+pipelines:
+ - name: pipeline
+ manager: IndependentPipelineManager
+ require:
+ all-approvals:
+ - username: '!jenkins'
+ trigger:
+ gerrit:
+ - event: comment-added
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: trigger
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: comment-added
+ require-all-approvals:
+ - username: '!jenkins'
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+projects:
+ - name: org/project1
+ pipeline:
+ - project1-pipeline
+ - name: org/project2
+ trigger:
+ - project2-trigger
\ No newline at end of file
diff --git a/tests/fixtures/layout-requirement-newer-than.yaml b/tests/fixtures/layout-requirement-newer-than.yaml
index b6beb35..f723c79 100644
--- a/tests/fixtures/layout-requirement-newer-than.yaml
+++ b/tests/fixtures/layout-requirement-newer-than.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
newer-than: 48h
trigger:
@@ -20,7 +20,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
newer-than: 48h
success:
diff --git a/tests/fixtures/layout-requirement-older-than.yaml b/tests/fixtures/layout-requirement-older-than.yaml
index 2edf9df..0e011cc 100644
--- a/tests/fixtures/layout-requirement-older-than.yaml
+++ b/tests/fixtures/layout-requirement-older-than.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
older-than: 48h
trigger:
@@ -20,7 +20,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
older-than: 48h
success:
diff --git a/tests/fixtures/layout-requirement-username.yaml b/tests/fixtures/layout-requirement-username.yaml
index 7a549f0..8520179 100644
--- a/tests/fixtures/layout-requirement-username.yaml
+++ b/tests/fixtures/layout-requirement-username.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
trigger:
gerrit:
@@ -19,7 +19,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
success:
gerrit:
diff --git a/tests/fixtures/layout-requirement-vote.yaml b/tests/fixtures/layout-requirement-vote.yaml
index 7ccadff..6736e98 100644
--- a/tests/fixtures/layout-requirement-vote.yaml
+++ b/tests/fixtures/layout-requirement-vote.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
verified: 1
trigger:
@@ -20,7 +20,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
verified: 1
success:
diff --git a/tests/fixtures/layout-requirement-vote1.yaml b/tests/fixtures/layout-requirement-vote1.yaml
index 7ccadff..6736e98 100644
--- a/tests/fixtures/layout-requirement-vote1.yaml
+++ b/tests/fixtures/layout-requirement-vote1.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
verified: 1
trigger:
@@ -20,7 +20,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
verified: 1
success:
diff --git a/tests/fixtures/layout-requirement-vote2.yaml b/tests/fixtures/layout-requirement-vote2.yaml
index 33d84d1..a6cd6a3 100644
--- a/tests/fixtures/layout-requirement-vote2.yaml
+++ b/tests/fixtures/layout-requirement-vote2.yaml
@@ -2,7 +2,7 @@
- name: pipeline
manager: IndependentPipelineManager
require:
- approval:
+ any-approval:
- username: jenkins
verified: [1, 2]
trigger:
@@ -20,7 +20,7 @@
trigger:
gerrit:
- event: comment-added
- require-approval:
+ require-any-approval:
- username: jenkins
verified: [1, 2]
success:
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index cc4d34c..1d23443 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -246,3 +246,7 @@
- name: org/experimental-project
experimental:
- experimental-project-test
+
+ - name: org/no-jobs-project
+ check:
+ - project-testfile
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index 120e37e..52e3973 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -52,13 +52,14 @@
self.assertEqual(len(self.history), 0)
# Add a too-old +1, should not be enqueued
- A.addApproval('VRFY', 1, granted_on=time.time() - 72 * 60 * 60)
+ A.addApproval('VRFY', 1, username='jenkins',
+ granted_on=time.time() - 72 * 60 * 60)
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 0)
# Add a recent +1
- self.fake_gerrit.addEvent(A.addApproval('VRFY', 1))
+ self.fake_gerrit.addEvent(A.addApproval('VRFY', 1, username='jenkins'))
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -95,7 +96,8 @@
self.assertEqual(len(self.history), 0)
# Add an old +1 which should be enqueued
- A.addApproval('VRFY', 1, granted_on=time.time() - 72 * 60 * 60)
+ A.addApproval('VRFY', 1, username='jenkins',
+ granted_on=time.time() - 72 * 60 * 60)
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -126,7 +128,7 @@
self.assertEqual(len(self.history), 0)
# Add an approval from Jenkins
- A.addApproval('VRFY', 1)
+ A.addApproval('VRFY', 1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -157,7 +159,7 @@
self.assertEqual(len(self.history), 0)
# Add an approval from Jenkins
- A.addApproval('VRFY', 1)
+ A.addApproval('VRFY', 1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -188,13 +190,13 @@
self.assertEqual(len(self.history), 0)
# A -1 from jenkins should not cause it to be enqueued
- A.addApproval('VRFY', -1)
+ A.addApproval('VRFY', -1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 0)
# A +1 should allow it to be enqueued
- A.addApproval('VRFY', 1)
+ A.addApproval('VRFY', 1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -225,19 +227,19 @@
self.assertEqual(len(self.history), 0)
# A -1 from jenkins should not cause it to be enqueued
- A.addApproval('VRFY', -1)
+ A.addApproval('VRFY', -1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 0)
# A -2 from jenkins should not cause it to be enqueued
- A.addApproval('VRFY', -2)
+ A.addApproval('VRFY', -2, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 0)
- # A +1 should allow it to be enqueued
- A.addApproval('VRFY', 1)
+ # A +1 from jenkins should allow it to be enqueued
+ A.addApproval('VRFY', 1, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
@@ -251,7 +253,7 @@
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
- B.addApproval('VRFY', 2)
+ B.addApproval('VRFY', 2, username='jenkins')
self.fake_gerrit.addEvent(comment)
self.waitUntilSettled()
self.assertEqual(len(self.history), 2)
@@ -321,3 +323,131 @@
self.fake_gerrit.addEvent(B.addApproval('CRVW', 2))
self.waitUntilSettled()
self.assertEqual(len(self.history), 1)
+
+ def test_pipeline_require_negative_username(self):
+ "Test negative pipeline requirement: no comment from jenkins"
+ return self._test_require_negative_username('org/project1',
+ 'project1-pipeline')
+
+ def test_trigger_require_negative_username(self):
+ "Test negative trigger requirement: no comment from jenkins"
+ return self._test_require_negative_username('org/project2',
+ 'project2-trigger')
+
+ def _test_require_negative_username(self, project, job):
+ "Test negative username's match"
+ # Should only trigger if Jenkins hasn't voted.
+ self.config.set(
+ 'zuul', 'layout_config',
+ 'tests/fixtures/layout-requirement-negative-username.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # add in a change with no comments
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # add in a comment that will trigger
+ self.fake_gerrit.addEvent(A.addApproval('CRVW', 1,
+ username='reviewer'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+ # add in a comment from jenkins user which shouldn't trigger
+ self.fake_gerrit.addEvent(A.addApproval('VRFY', 1, username='jenkins'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # Check future reviews also won't trigger as a 'jenkins' user has
+ # commented previously
+ self.fake_gerrit.addEvent(A.addApproval('CRVW', 1,
+ username='reviewer'))
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ def test_pipeline_require_any(self):
+ "Test pipeline requirement: any requirement passes"
+ return self._test_require_any('org/project1', 'project1-pipeline')
+
+ def test_trigger_require_any(self):
+ "Test trigger requirement: any requirement passes"
+ return self._test_require_any('org/project2', 'project2-trigger')
+
+ def _test_require_any(self, project, job):
+ "Test any of the given requirements are matched"
+ self.config.set(
+ 'zuul', 'layout_config',
+ 'tests/fixtures/layout-requirement-any.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ # A comment event that we will keep submitting to trigger
+ comment = A.addApproval('CRVW', 1, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ # No approval from Jenkins so should not be enqueued
+ self.assertEqual(len(self.history), 0)
+
+ # A +1 from jenkins should allow it to be enqueued
+ A.addApproval('VRFY', 1, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
+
+ # A non-negative from a non-core should not queue
+ B = self.fake_gerrit.addFakeChange(project, 'master', 'B')
+ # A comment event that we will keep submitting to trigger
+ comment = B.addApproval('CRVW', 1, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+
+ # A non-negative from a core member should queue
+ B.addApproval('CRVW', 2, username='core-reviewer')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 2)
+ self.assertEqual(self.history[1].name, job)
+
+ def test_pipeline_require_all(self):
+ "Test pipeline requirement: all requirements pass"
+ return self._test_require_all('org/project1', 'project1-pipeline')
+
+ def test_trigger_require_all(self):
+ "Test trigger requirement: all requirements pass"
+ return self._test_require_all('org/project2', 'project2-trigger')
+
+ def _test_require_all(self, project, job):
+ "Test all of the given requirements are matched"
+ self.config.set(
+ 'zuul', 'layout_config',
+ 'tests/fixtures/layout-requirement-all.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A +2 from 'nobody' only satisfies the non-negative requirement,
+ # not the requirement to be from 'jenkins'
+ comment = A.addApproval('VRFY', 1, username='nobody')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ B = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 0)
+
+ # A +2 from Jenkins satisfies both the user condition and the
+ # non-negative condition
+ comment = B.addApproval('VRFY', 2, username='jenkins')
+ self.fake_gerrit.addEvent(comment)
+ self.waitUntilSettled()
+ self.assertEqual(len(self.history), 1)
+ self.assertEqual(self.history[0].name, job)
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 3b59e3e..5b9a39d 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -1878,6 +1878,23 @@
self.assertEqual(A.data['status'], 'MERGED')
self.assertEqual(A.reported, 2)
+ def test_no_job_project(self):
+ "Test that reports with no jobs don't get sent"
+ A = self.fake_gerrit.addFakeChange('org/no-jobs-project',
+ 'master', 'A')
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ # Change wasn't reported to
+ self.assertEqual(A.reported, False)
+
+ # Check queue is empty afterwards
+ check_pipeline = self.sched.layout.pipelines['check']
+ items = check_pipeline.getAllItems()
+ self.assertEqual(len(items), 0)
+
+ self.assertEqual(len(self.history), 0)
+
def test_zuul_refs(self):
"Test that zuul refs exist and have the right changes"
self.worker.hold_jobs_in_build = True
@@ -2019,6 +2036,30 @@
self.assertEqual(self.history[0].name, 'gate-noop')
self.assertEqual(self.history[0].result, 'SUCCESS')
+ def test_file_head(self):
+ # This is a regression test for an observed bug. A change
+ # with a file named "HEAD" in the root directory of the repo
+ # was processed by a merger. It then was unable to reset the
+ # repo because of:
+ # GitCommandError: 'git reset --hard HEAD' returned
+ # with exit code 128
+ # stderr: 'fatal: ambiguous argument 'HEAD': both revision
+ # and filename
+ # Use '--' to separate filenames from revisions'
+
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['HEAD'])
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+
+ self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
+ self.waitUntilSettled()
+
+ self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+ self.waitUntilSettled()
+
+ self.assertIn('Build succeeded', A.messages[0])
+ self.assertIn('Build succeeded', B.messages[0])
+
def test_file_jobs(self):
"Test that file jobs run only when appropriate"
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
@@ -2224,6 +2265,127 @@
self.assertEqual(A.data['status'], 'MERGED')
self.assertEqual(A.reported, 2)
+ def test_live_reconfiguration_merge_conflict(self):
+ # A real-world bug: a change in a gate queue has a merge
+ # conflict and a job is added to its project while it's
+ # sitting in the queue. The job gets added to the change and
+ # enqueued and the change gets stuck.
+ self.worker.registerFunction('build:project-test3')
+ self.worker.hold_jobs_in_build = True
+
+ # This change is fine. It's here to stop the queue long
+ # enough for the next change to be subject to the
+ # reconfiguration, as well as to provide a conflict for the
+ # next change. This change will succeed and merge.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ A.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+
+ # This change will be in merge conflict. During the
+ # reconfiguration, we will add a job. We want to make sure
+ # that doesn't cause it to get stuck.
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ B.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+
+ self.waitUntilSettled()
+
+ # No jobs have run yet
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(len(self.history), 0)
+
+ # Add the "project-test3" job.
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-live-'
+ 'reconfiguration-add-job.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(self.getJobFromHistory('project-merge').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test1').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test2').result,
+ 'SUCCESS')
+ self.assertEqual(self.getJobFromHistory('project-test3').result,
+ 'SUCCESS')
+ self.assertEqual(len(self.history), 4)
+
+ def test_live_reconfiguration_failed_job(self):
+ # An extrapolation of test_live_reconfiguration_merge_conflict
+ # that tests a job added to a job tree with a failed root does
+ # not run.
+ self.worker.registerFunction('build:project-test3')
+ self.worker.hold_jobs_in_build = True
+
+ # This change is fine. It's here to stop the queue long
+ # enough for the next change to be subject to the
+ # reconfiguration. This change will succeed and merge.
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ A.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.waitUntilSettled()
+ self.worker.release('.*-merge')
+ self.waitUntilSettled()
+
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ self.worker.addFailTest('project-merge', B)
+ B.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.worker.release('.*-merge')
+ self.waitUntilSettled()
+
+ # Both -merge jobs have run, but no others.
+ self.assertEqual(A.data['status'], 'NEW')
+ self.assertEqual(A.reported, 1)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 1)
+ self.assertEqual(self.history[0].result, 'SUCCESS')
+ self.assertEqual(self.history[0].name, 'project-merge')
+ self.assertEqual(self.history[1].result, 'FAILURE')
+ self.assertEqual(self.history[1].name, 'project-merge')
+ self.assertEqual(len(self.history), 2)
+
+ # Add the "project-test3" job.
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-live-'
+ 'reconfiguration-add-job.yaml')
+ self.sched.reconfigure(self.config)
+ self.waitUntilSettled()
+
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ self.assertEqual(A.data['status'], 'MERGED')
+ self.assertEqual(A.reported, 2)
+ self.assertEqual(B.data['status'], 'NEW')
+ self.assertEqual(B.reported, 2)
+ self.assertEqual(self.history[0].result, 'SUCCESS')
+ self.assertEqual(self.history[0].name, 'project-merge')
+ self.assertEqual(self.history[1].result, 'FAILURE')
+ self.assertEqual(self.history[1].name, 'project-merge')
+ self.assertEqual(self.history[2].result, 'SUCCESS')
+ self.assertEqual(self.history[3].result, 'SUCCESS')
+ self.assertEqual(self.history[4].result, 'SUCCESS')
+ self.assertEqual(len(self.history), 5)
+
def test_live_reconfiguration_functions(self):
"Test live reconfiguration with a custom function"
self.worker.registerFunction('build:node-project-test1:debian')
diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py
index 653678a..57ac5ca 100644
--- a/zuul/launcher/gearman.py
+++ b/zuul/launcher/gearman.py
@@ -265,12 +265,14 @@
params))
def launch(self, job, item, pipeline, dependent_items=[]):
- self.log.info("Launch job %s for change %s with dependent changes %s" %
- (job, item.change,
- [x.change for x in dependent_items]))
+ uuid = str(uuid4().hex)
+ self.log.info(
+ "Launch job %s (uuid: %s) for change %s with dependent "
+ "changes %s" % (
+ job, uuid, item.change,
+ [x.change for x in dependent_items]))
dependent_items = dependent_items[:]
dependent_items.reverse()
- uuid = str(uuid4().hex)
params = dict(ZUUL_UUID=uuid,
ZUUL_PROJECT=item.change.project.name)
params['ZUUL_PIPELINE'] = pipeline.name
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 88d10e2..1569fa9 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -62,6 +62,8 @@
'branch': toList(str),
'ref': toList(str),
'approval': toList(variable_dict),
+ 'require-all-approvals': toList(require_approval),
+ 'require-any-approval': toList(require_approval),
'require-approval': toList(require_approval),
}
@@ -85,7 +87,9 @@
},
}
- require = {'approval': toList(require_approval),
+ require = {'all-approvals': toList(require_approval),
+ 'any-approval': toList(require_approval),
+ 'approval': toList(require_approval),
'open': bool,
'current-patchset': bool,
'status': toList(str)}
diff --git a/zuul/merger/merger.py b/zuul/merger/merger.py
index f36b974..f571ad0 100644
--- a/zuul/merger/merger.py
+++ b/zuul/merger/merger.py
@@ -20,6 +20,21 @@
import zuul.model
+def reset_repo_to_head(repo):
+ # This lets us reset the repo even if there is a file in the root
+ # directory named 'HEAD'. Currently, GitPython does not allow us
+ # to instruct it to always include the '--' to disambiguate. This
+ # should no longer be necessary if this PR merges:
+ # https://github.com/gitpython-developers/GitPython/pull/319
+ try:
+ repo.git.reset('--hard', 'HEAD', '--')
+ except git.GitCommandError as e:
+ # git nowadays may use 1 as status to indicate there are still unstaged
+ # modifications after the reset
+ if e.status != 1:
+ raise
+
+
class ZuulReference(git.Reference):
_common_path_default = "refs/zuul"
_points_to_commits_only = True
@@ -82,7 +97,7 @@
# Reset to remote HEAD (usually origin/master)
repo.head.reference = origin.refs['HEAD']
- repo.head.reset(index=True, working_tree=True)
+ reset_repo_to_head(repo)
repo.git.clean('-x', '-f', '-d')
def prune(self):
@@ -114,7 +129,7 @@
repo = self.createRepoObject()
self.log.debug("Checking out %s" % ref)
repo.head.reference = ref
- repo.head.reset(index=True, working_tree=True)
+ reset_repo_to_head(repo)
def cherryPick(self, ref):
repo = self.createRepoObject()
diff --git a/zuul/model.py b/zuul/model.py
index 4d402ff..6648774 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -12,8 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+import ast
import copy
import re
+import six
import time
from uuid import uuid4
import extras
@@ -735,7 +737,13 @@
ret['items_behind'] = [i.change._id() for i in self.items_behind]
ret['failing_reasons'] = self.current_build_set.failing_reasons
ret['zuul_ref'] = self.current_build_set.ref
- ret['project'] = changeish.project.name
+ if changeish.project:
+ ret['project'] = changeish.project.name
+ else:
+ # For cross-project dependencies with the depends-on
+ # project not known to zuul, the project is None
+ # Set it to a static value
+ ret['project'] = "Unknown Project"
ret['enqueue_time'] = int(self.enqueue_time * 1000)
ret['jobs'] = []
if hasattr(changeish, 'owner'):
@@ -1019,68 +1027,127 @@
class BaseFilter(object):
- def __init__(self, required_approvals=[]):
- self._required_approvals = copy.deepcopy(required_approvals)
- self.required_approvals = required_approvals
+ def __init__(self, required_any_approval=[], required_all_approvals=[]):
+ self._required_any_approval = copy.deepcopy(required_any_approval)
+ self.required_any_approval = self._tidy_approvals(
+ required_any_approval)
+ self._required_all_approvals = copy.deepcopy(required_all_approvals)
+ self.required_all_approvals = self._tidy_approvals(
+ required_all_approvals)
- for a in self.required_approvals:
+ def _tidy_approvals(self, approvals):
+ for a in approvals:
for k, v in a.items():
if k == 'username':
pass
elif k in ['email', 'email-filter']:
- a['email'] = re.compile(v)
+ a['email'] = v
elif k == 'newer-than':
- a[k] = time_to_seconds(v)
+ a[k] = v
elif k == 'older-than':
- a[k] = time_to_seconds(v)
- else:
- if not isinstance(v, list):
- a[k] = [v]
+ a[k] = v
if 'email-filter' in a:
del a['email-filter']
+ return approvals
+
+ def _match_approval_required_approval(self, rapproval, approval):
+ # Check if the required approval and approval match
+ if 'description' not in approval:
+ return False
+ now = time.time()
+ found_approval = True
+ by = approval.get('by', {})
+ for k, v in rapproval.items():
+ negative_match = False
+ item_match = True
+ if isinstance(v, six.string_types) and v[0] == '!':
+ v = v[1:].strip()
+ item_match = False
+ negative_match = True
+
+ if k == 'username':
+ if (by.get('username', '') != v):
+ item_match = negative_match
+ elif k == 'email':
+ v = re.compile(v)
+ if (not v.search(by.get('email', ''))):
+ item_match = negative_match
+ elif k == 'newer-than':
+ t = now - time_to_seconds(v)
+ if (approval['grantedOn'] < t):
+ item_match = negative_match
+ elif k == 'older-than':
+ t = now - time_to_seconds(v)
+ if (approval['grantedOn'] >= t):
+ item_match = negative_match
+ else:
+ if isinstance(v, six.string_types):
+ v = ast.literal_eval(v)
+ if not isinstance(v, list):
+ v = [v]
+ if (normalizeCategory(approval['description']) != k or
+ int(approval['value']) not in v):
+ item_match = negative_match
+ if not item_match:
+ found_approval = False
+ return found_approval
def matchesRequiredApprovals(self, change):
- now = time.time()
- for rapproval in self.required_approvals:
+ if (self.required_any_approval and not change.approvals
+ or self.required_all_approvals and not change.approvals):
+ # A change with no approvals can not match
+ return False
+
+ # TODO(jhesketh): If we wanted to optimise this slightly we could
+ # analyse both the ANY and ALL filters by looping over the approvals
+ # on the change and keeping track of what we have checked rather than
+ # needing to loop on the change approvals twice
+ return (self.matchesRequiredAnyApproval(change) and
+ self.matchesRequiredAllApprovals(change))
+
+ def matchesRequiredAnyApproval(self, change):
+ # Check if any approvals match the any requirements
+ if not self.required_any_approval:
+ # No approval required, so we must match
+ return True
+
+ for rapproval in self.required_any_approval:
matches_approval = False
for approval in change.approvals:
- if 'description' not in approval:
- continue
- found_approval = True
- by = approval.get('by', {})
- for k, v in rapproval.items():
- if k == 'username':
- if (by.get('username', '') != v):
- found_approval = False
- elif k == 'email':
- if (not v.search(by.get('email', ''))):
- found_approval = False
- elif k == 'newer-than':
- t = now - v
- if (approval['grantedOn'] < t):
- found_approval = False
- elif k == 'older-than':
- t = now - v
- if (approval['grantedOn'] >= t):
- found_approval = False
- else:
- if (normalizeCategory(approval['description']) != k or
- int(approval['value']) not in v):
- found_approval = False
- if found_approval:
- matches_approval = True
- break
- if not matches_approval:
- return False
+ matches_approval = self._match_approval_required_approval(
+ rapproval, approval)
+ if matches_approval:
+ # We have a matching approval so this requirement is
+ # fulfilled
+ return True
+ return False
+
+ def matchesRequiredAllApprovals(self, change):
+ # Check that /all/ of the approvals match the requirements
+ if not self.required_all_approvals:
+ # No approvals required, so we must match
+ return True
+
+ for rapproval in self.required_all_approvals:
+ for approval in change.approvals:
+ matches_approval = self._match_approval_required_approval(
+ rapproval, approval)
+ if not matches_approval:
+ # We have an approval that doesn't match so this
+ # requirement can't be fulfilled
+ return False
+ # We must have matched everything
return True
class EventFilter(BaseFilter):
def __init__(self, trigger, types=[], branches=[], refs=[],
event_approvals={}, comments=[], emails=[], usernames=[],
- timespecs=[], required_approvals=[], pipelines=[]):
+ timespecs=[], required_any_approval=[],
+ required_all_approvals=[], pipelines=[]):
super(EventFilter, self).__init__(
- required_approvals=required_approvals)
+ required_any_approval=required_any_approval,
+ required_all_approvals=required_all_approvals)
self.trigger = trigger
self._types = types
self._branches = branches
@@ -1113,9 +1180,12 @@
if self.event_approvals:
ret += ' event_approvals: %s' % ', '.join(
['%s:%s' % a for a in self.event_approvals.items()])
- if self.required_approvals:
- ret += ' required_approvals: %s' % ', '.join(
- ['%s' % a for a in self._required_approvals])
+ if self.required_any_approval:
+ ret += ' required_any_approval: %s' % ', '.join(
+ ['%s' % a for a in self._required_any_approval])
+ if self.required_all_approvals:
+ ret += ' required_all_approvals: %s' % ', '.join(
+ ['%s' % a for a in self._required_all_approvals])
if self._comments:
ret += ' comments: %s' % ', '.join(self._comments)
if self._emails:
@@ -1204,10 +1274,6 @@
if not matches_approval:
return False
- if self.required_approvals and not change.approvals:
- # A change with no approvals can not match
- return False
-
# required approvals are ANDed
if not self.matchesRequiredApprovals(change):
return False
@@ -1225,9 +1291,11 @@
class ChangeishFilter(BaseFilter):
def __init__(self, open=None, current_patchset=None,
- statuses=[], required_approvals=[]):
+ statuses=[], required_any_approval=[],
+ required_all_approvals=[]):
super(ChangeishFilter, self).__init__(
- required_approvals=required_approvals)
+ required_any_approval=required_any_approval,
+ required_all_approvals=required_all_approvals)
self.open = open
self.current_patchset = current_patchset
self.statuses = statuses
@@ -1241,8 +1309,12 @@
ret += ' current-patchset: %s' % self.current_patchset
if self.statuses:
ret += ' statuses: %s' % ', '.join(self.statuses)
- if self.required_approvals:
- ret += ' required_approvals: %s' % str(self.required_approvals)
+ if self.required_any_approval:
+ ret += (' required_any_approval: %s' %
+ str(self.required_any_approval))
+ if self.required_all_approvals:
+ ret += (' required_all_approvals: %s' %
+ str(self.required_all_approvals))
ret += '>'
return ret
@@ -1260,10 +1332,6 @@
if change.status not in self.statuses:
return False
- if self.required_approvals and not change.approvals:
- # A change with no approvals can not match
- return False
-
# required approvals are ANDed
if not self.matchesRequiredApprovals(change):
return False
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 131ad62..2d074cc 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -198,7 +198,7 @@
self.management_event_queue = Queue.Queue()
self.layout = model.Layout()
- self.zuul_version = zuul_version.version_info.version_string()
+ self.zuul_version = zuul_version.version_info.release_string()
self.last_reconfigured = None
def stop(self):
@@ -326,7 +326,10 @@
open=require.get('open'),
current_patchset=require.get('current-patchset'),
statuses=toList(require.get('status')),
- required_approvals=toList(require.get('approval')))
+ required_any_approval=(toList(require.get('any-approval'))
+ + toList(require.get('approval'))),
+ required_all_approvals=toList(require.get('all-approvals'))
+ )
manager.changeish_filters.append(f)
# TODO: move this into triggers (may require pluggable
@@ -356,9 +359,13 @@
comments=comments,
emails=emails,
usernames=usernames,
- required_approvals=toList(
- trigger.get('require-approval')
- )
+ required_any_approval=(
+ toList(trigger.get('require-any-approval'))
+ + toList(trigger.get('require-approval'))
+ ),
+ required_all_approvals=toList(
+ trigger.get('require-all-approvals')
+ ),
)
manager.event_filters.append(f)
if 'timer' in conf_pipeline['trigger']:
@@ -373,9 +380,13 @@
trigger=self.triggers['zuul'],
types=toList(trigger['event']),
pipelines=toList(trigger.get('pipeline')),
- required_approvals=toList(
- trigger.get('require-approval')
- )
+ required_any_approval=(
+ toList(trigger.get('require-any-approval'))
+ + toList(trigger.get('require-approval'))
+ ),
+ required_all_approvals=toList(
+ trigger.get('require-all-approvals')
+ ),
)
manager.event_filters.append(f)
@@ -1176,6 +1187,18 @@
self.log.debug("Re-enqueing change %s in queue %s" %
(item.change, change_queue))
change_queue.enqueueItem(item)
+
+ # Re-set build results in case any new jobs have been
+ # added to the tree.
+ for build in item.current_build_set.getBuilds():
+ if build.result:
+ self.pipeline.setResult(item, build)
+ # Similarly, reset the item state.
+ if item.current_build_set.unable_to_merge:
+ self.pipeline.setUnableToMerge(item)
+ if item.dequeued_needing_change:
+ self.pipeline.setDequeuedNeedingChange(item)
+
self.reportStats(item)
return True
else:
@@ -1521,7 +1544,13 @@
def _reportItem(self, item):
self.log.debug("Reporting change %s" % item.change)
ret = True # Means error as returned by trigger.report
- if self.pipeline.didAllJobsSucceed(item):
+ if not self.pipeline.getJobs(item):
+ # We don't send empty reports with +1,
+ # and the same for -1's (merge failures or transient errors)
+ # as they cannot be followed by +1's
+ self.log.debug("No jobs for change %s" % item.change)
+ actions = []
+ elif self.pipeline.didAllJobsSucceed(item):
self.log.debug("success %s" % (self.pipeline.success_actions))
actions = self.pipeline.success_actions
item.setReportedResult('SUCCESS')