From 13f843a165a39f0362eeaf83c09bdaef5eb43eed Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 19 Oct 2018 17:24:01 +0200 Subject: [PATCH] [IMP] runbot_merge: adds direct links to CI details to the dashboard Currently, if a staging is ongoing or failed one has to hunt for the staging branches on the runbot dashboard in order to find out what happens. This adds a dropdown to the staging box/block providing direct status and access to all the CI information whether the CI is ongoing or done, successful or not. --- runbot_merge/models/pull_requests.py | 26 +++++++++++++++++++++++++ runbot_merge/tests/local.py | 16 ++++++++++++++- runbot_merge/tests/remote.py | 9 +++++++++ runbot_merge/tests/test_basic.py | 21 ++++++++++++++++++-- runbot_merge/views/templates.xml | 29 ++++++++++++++++++++++++---- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/runbot_merge/models/pull_requests.py b/runbot_merge/models/pull_requests.py index 9c012089..988b21f8 100644 --- a/runbot_merge/models/pull_requests.py +++ b/runbot_merge/models/pull_requests.py @@ -900,6 +900,32 @@ class Stagings(models.Model): # seems simpler than adding yet another indirection through a model heads = fields.Char(required=True, help="JSON-encoded map of heads, one per repo in the project") + statuses = fields.Binary(compute='_compute_statuses') + + @api.depends('heads') + def _compute_statuses(self): + """ Fetches statuses associated with the various heads, returned as + (repo, context, state, url) + """ + Commits = self.env['runbot_merge.commit'] + for st in self: + heads = { + head: repo for repo, head in json.loads(st.heads).items() + if not repo.endswith('^') + } + commits = Commits.search([('sha', 'in', list(heads.keys()))]) + st.statuses = [ + ( + heads[commit.sha], + context, + status.get('state') or 'pending', + status.get('target_url') or '' + ) + for commit in commits + for context, st in json.loads(commit.statuses).items() + for status in [to_status(st)] + ] + def _validate(self): Commits = self.env['runbot_merge.commit'] for s in self: diff --git a/runbot_merge/tests/local.py b/runbot_merge/tests/local.py index abb06a14..db3b14dc 100644 --- a/runbot_merge/tests/local.py +++ b/runbot_merge/tests/local.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- -import odoo + import pytest +import werkzeug.test, werkzeug.wrappers + +import odoo + import fake_github @pytest.fixture(scope='session') @@ -96,6 +100,16 @@ def make_repo(gh, project): ]) ]) return make_repo + +@pytest.fixture +def page(): + c = werkzeug.test.Client(odoo.http.root, werkzeug.wrappers.BaseResponse) + def get(url): + r = c.get(url) + assert r.status_code == 200 + return r.data + return get + # TODO: project fixture # TODO: repos (indirect/parameterize?) w/ WS hook # + repo proxy object diff --git a/runbot_merge/tests/remote.py b/runbot_merge/tests/remote.py index 2e2b4f59..386de55b 100644 --- a/runbot_merge/tests/remote.py +++ b/runbot_merge/tests/remote.py @@ -93,6 +93,15 @@ def wait_for_hook(n=1): # TODO: find better way to wait for roundtrip of actions which can trigger webhooks time.sleep(10 * n) +@pytest.fixture +def page(): + s = requests.Session() + def get(url): + r = s.get('http://localhost:{}{}'.format(PORT, url)) + r.raise_for_status() + return r.content + return get + def wait_for_server(db, timeout=120): """ Polls for server to be response & have installed our module. diff --git a/runbot_merge/tests/test_basic.py b/runbot_merge/tests/test_basic.py index 3ffdf23d..ac227aaf 100644 --- a/runbot_merge/tests/test_basic.py +++ b/runbot_merge/tests/test_basic.py @@ -6,6 +6,7 @@ import time from unittest import mock import pytest +from lxml import html import odoo @@ -15,7 +16,7 @@ from test_utils import re_matches def repo(make_repo): return make_repo('repo') -def test_trivial_flow(env, repo): +def test_trivial_flow(env, repo, page): # create base branch m = repo.make_commit(None, "initial", None, tree={'a': 'some content'}) repo.make_ref('heads/master', m) @@ -58,8 +59,24 @@ def test_trivial_flow(env, repo): # get head of staging branch staging_head = repo.commit('heads/staging.master') - repo.post_status(staging_head.id, 'success', 'ci/runbot') + repo.post_status(staging_head.id, 'success', 'ci/runbot', target_url='http://foo.com/pog') repo.post_status(staging_head.id, 'success', 'legal/cla') + # the should not block the merge because it's not part of the requirements + repo.post_status(staging_head.id, 'failure', 'ci/lint', target_url='http://ignored.com/whocares') + + assert set(tuple(t) for t in pr.staging_id.statuses) == { + (repo.name, 'legal/cla', 'success', ''), + (repo.name, 'ci/runbot', 'success', 'http://foo.com/pog'), + (repo.name, 'ci/lint', 'failure', 'http://ignored.com/whocares'), + } + p = html.fromstring(page('/runbot_merge')) + s = p.cssselect('.staging div.dropdown li') + assert len(s) == 2 + assert s[0].get('class') == 'bg-success' + assert s[0][0].text.strip() == '{}: ci/runbot'.format(repo.name) + assert s[1].get('class') == 'bg-danger' + assert s[1][0].text.strip() == '{}: ci/lint'.format(repo.name) + assert re.match('^force rebuild', staging_head.message) env['runbot_merge.project']._check_progress() diff --git a/runbot_merge/views/templates.xml b/runbot_merge/views/templates.xml index 72ebcea8..f0982a65 100644 --- a/runbot_merge/views/templates.xml +++ b/runbot_merge/views/templates.xml @@ -5,7 +5,6 @@ .stagings { display: flex; align-items: stretch; - overflow: hidden; } .stagings > li { /* 4 to 6 stagings displayed, avoid stretching */ @@ -86,9 +85,9 @@ Cancelled: Fast Forward Failed - + -
  • +
    • @@ -99,7 +98,29 @@
    - Staged +