mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[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.
This commit is contained in:
parent
02dd03fca3
commit
13f843a165
@ -900,6 +900,32 @@ class Stagings(models.Model):
|
|||||||
# seems simpler than adding yet another indirection through a 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")
|
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):
|
def _validate(self):
|
||||||
Commits = self.env['runbot_merge.commit']
|
Commits = self.env['runbot_merge.commit']
|
||||||
for s in self:
|
for s in self:
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import odoo
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import werkzeug.test, werkzeug.wrappers
|
||||||
|
|
||||||
|
import odoo
|
||||||
|
|
||||||
import fake_github
|
import fake_github
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
@ -96,6 +100,16 @@ def make_repo(gh, project):
|
|||||||
])
|
])
|
||||||
])
|
])
|
||||||
return make_repo
|
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: project fixture
|
||||||
# TODO: repos (indirect/parameterize?) w/ WS hook
|
# TODO: repos (indirect/parameterize?) w/ WS hook
|
||||||
# + repo proxy object
|
# + repo proxy object
|
||||||
|
@ -93,6 +93,15 @@ def wait_for_hook(n=1):
|
|||||||
# TODO: find better way to wait for roundtrip of actions which can trigger webhooks
|
# TODO: find better way to wait for roundtrip of actions which can trigger webhooks
|
||||||
time.sleep(10 * n)
|
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):
|
def wait_for_server(db, timeout=120):
|
||||||
""" Polls for server to be response & have installed our module.
|
""" Polls for server to be response & have installed our module.
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import time
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
import odoo
|
import odoo
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ from test_utils import re_matches
|
|||||||
def repo(make_repo):
|
def repo(make_repo):
|
||||||
return make_repo('repo')
|
return make_repo('repo')
|
||||||
|
|
||||||
def test_trivial_flow(env, repo):
|
def test_trivial_flow(env, repo, page):
|
||||||
# create base branch
|
# create base branch
|
||||||
m = repo.make_commit(None, "initial", None, tree={'a': 'some content'})
|
m = repo.make_commit(None, "initial", None, tree={'a': 'some content'})
|
||||||
repo.make_ref('heads/master', m)
|
repo.make_ref('heads/master', m)
|
||||||
@ -58,8 +59,24 @@ def test_trivial_flow(env, repo):
|
|||||||
|
|
||||||
# get head of staging branch
|
# get head of staging branch
|
||||||
staging_head = repo.commit('heads/staging.master')
|
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')
|
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)
|
assert re.match('^force rebuild', staging_head.message)
|
||||||
|
|
||||||
env['runbot_merge.project']._check_progress()
|
env['runbot_merge.project']._check_progress()
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
.stagings {
|
.stagings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.stagings > li {
|
.stagings > li {
|
||||||
/* 4 to 6 stagings displayed, avoid stretching */
|
/* 4 to 6 stagings displayed, avoid stretching */
|
||||||
@ -86,9 +85,9 @@
|
|||||||
<t t-set="title">
|
<t t-set="title">
|
||||||
<t t-if="staging.state == 'canceled'">Cancelled: <t t-esc="staging.reason"/></t>
|
<t t-if="staging.state == 'canceled'">Cancelled: <t t-esc="staging.reason"/></t>
|
||||||
<t t-if="staging.state == 'ff_failed'">Fast Forward Failed</t>
|
<t t-if="staging.state == 'ff_failed'">Fast Forward Failed</t>
|
||||||
<t t-if="staging.state == 'failure'"><t t-esc="staging.reason"/></t>
|
<t t-if="staging.state not in ('canceled', 'ff_failed')"><t t-esc="staging.reason"/></t>
|
||||||
</t>
|
</t>
|
||||||
<li t-attf-class="{{stateclass}} {{decorationclass}}" t-att-title="title.strip() or None">
|
<li t-attf-class="staging {{stateclass}} {{decorationclass}}" t-att-title="title.strip() or None">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li t-foreach="staging.batch_ids" t-as="batch" class="batch">
|
<li t-foreach="staging.batch_ids" t-as="batch" class="batch">
|
||||||
<t t-esc="batch.prs[:1].label"/>
|
<t t-esc="batch.prs[:1].label"/>
|
||||||
@ -99,7 +98,29 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<t t-if="staging.heads">
|
<t t-if="staging.heads">
|
||||||
Staged <span t-field="staging.staged_at" t-options="{'widget': 'relative'}"/>
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||||
|
Staged <span t-field="staging.staged_at" t-options="{'widget': 'relative'}"/>
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li groups="runbot_merge.group_admin">
|
||||||
|
<a t-attf-href="/web#id={{staging.id}}&view_type=form&model=runbot_merge.stagings" target="new">
|
||||||
|
Open Staging
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li t-foreach="staging.statuses" t-as="st" t-if="st[3]"
|
||||||
|
t-att-class="
|
||||||
|
'bg-success' if st[2] == 'success'
|
||||||
|
else 'bg-danger' if st[2] in ('error', 'failure')
|
||||||
|
else 'bg-info'"
|
||||||
|
>
|
||||||
|
<a t-att-href="st[3]" target="new">
|
||||||
|
<t t-esc="st[0]"/>: <t t-esc="st[1]"/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</li>
|
</li>
|
||||||
</t>
|
</t>
|
||||||
|
Loading…
Reference in New Issue
Block a user