Support dynamic badges

Zuul currently has a zuul/gated badge that can be linked e.g. in a
readme of a project. This is sufficient for many use cases. However if
a project has periodic jobs that do extended testing which is not
possible in check/gate this is not sufficient. For use cases like
those we can add support for dynamic badges in zuul itself.

Change-Id: I449fa9f38ca251522789b6075fbc876d21bd0200
This commit is contained in:
Tobias Henkel 2020-01-12 11:21:10 +01:00
parent 551dbcbbc6
commit 5350525e65
7 changed files with 141 additions and 0 deletions

View File

@ -16,3 +16,10 @@ report, it is a simple static file:
To use it, simply put ``https://zuul-ci.org/gated.svg`` into an RST or
markdown formatted README file, or use it in an ``<img>`` tag in HTML.
For advanced usage Zuul also supports generating dynamic badges via the
REST api. This can be useful if you want to display the status of e.g. periodic
pipelines of a project. To use it use an url like
``https://zuul.opendev.org/api/tenant/zuul/badge?project=zuul/zuul-website&pipeline=post``
instead of the above mentioned url. It supports filtering by ``project``,
``pipeline`` and ``branch``.

View File

@ -0,0 +1,4 @@
---
features:
- |
Support for generating dynamic :ref:`badges` has been added.

View File

@ -1126,6 +1126,39 @@ class TestBuildInfo(ZuulDBTestCase, BaseTestWeb):
resp = self.get_url("api/tenant/non-tenant/builds")
self.assertEqual(404, resp.status_code)
def test_web_badge(self):
# Generate some build records in the db.
self.add_base_changes()
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
# Now request badge for the buildsets
result = self.get_url("api/tenant/tenant-one/badge")
self.log.error(result.content)
result.raise_for_status()
self.assertTrue(result.text.startswith('<svg '))
self.assertIn('passing', result.text)
# Generate a failing record
self.executor_server.hold_jobs_in_build = True
C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
C.addApproval('Code-Review', 2)
self.fake_gerrit.addEvent(C.addApproval('Approved', 1))
self.waitUntilSettled()
self.executor_server.failJob('project-merge', C)
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
# Request again badge for the buildsets
result = self.get_url("api/tenant/tenant-one/badge")
self.log.error(result.content)
result.raise_for_status()
self.assertTrue(result.text.startswith('<svg '))
self.assertIn('failing', result.text)
def test_web_list_buildsets(self):
# Generate some build records in the db.
self.add_base_changes()

18
web/public/failing.svg Normal file
View File

@ -0,0 +1,18 @@
<svg width="80.2" height="20" viewBox="0 0 802 200" xmlns="http://www.w3.org/2000/svg">
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="m"><rect width="802" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#m)">
<rect width="368" height="200" fill="#555"/>
<rect width="434" height="200" fill="#E43" x="368"/>
<rect width="802" height="200" fill="url(#a)"/>
</g>
<g fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="60" y="148" textLength="268" fill="#000" opacity="0.25">build</text>
<text x="50" y="138" textLength="268">build</text>
<text x="423" y="148" textLength="334" fill="#000" opacity="0.25">failing</text>
<text x="413" y="138" textLength="334">failing</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -6,6 +6,44 @@ openapi: 3.0.0
tags:
- name: tenant
paths:
/api/tenant/{tenant}/badge:
get:
operationId: get-badge
parameters:
- description: The tenant name
in: path
name: tenant
required: true
schema:
type: string
- description: A project name
in: query
name: project
schema:
type: string
- description: A pipeline name
in: query
name: pipeline
schema:
type: string
- description: A branch name
in: query
name: branch
schema:
type: string
responses:
'200':
content:
image/svg+xml:
schema:
description: SVG image
type: object
description: Badge describing the result of the latest buildset found.
'404':
description: No buildset found
summary: Get a badge describing the result of the latest buildset found.
tags:
- tenant
/api/tenant/{tenant}/builds:
get:
operationId: list-builds

18
web/public/passing.svg Normal file
View File

@ -0,0 +1,18 @@
<svg width="88.6" height="20" viewBox="0 0 886 200" xmlns="http://www.w3.org/2000/svg">
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="m"><rect width="886" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#m)">
<rect width="368" height="200" fill="#555"/>
<rect width="518" height="200" fill="#3C1" x="368"/>
<rect width="886" height="200" fill="url(#a)"/>
</g>
<g fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="60" y="148" textLength="268" fill="#000" opacity="0.25">build</text>
<text x="50" y="138" textLength="268">build</text>
<text x="423" y="148" textLength="418" fill="#000" opacity="0.25">passing</text>
<text x="413" y="138" textLength="418">passing</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -529,6 +529,7 @@ class ZuulWebAPI(object):
'project_ssh_key': '/api/tenant/{tenant}/project-ssh-key/'
'{project:.*}.pub',
'console_stream': '/api/tenant/{tenant}/console-stream',
'badge': '/api/tenant/{tenant}/badge',
'builds': '/api/tenant/{tenant}/builds',
'build': '/api/tenant/{tenant}/build/{uuid}',
'buildsets': '/api/tenant/{tenant}/buildsets',
@ -927,6 +928,26 @@ class ZuulWebAPI(object):
ret['builds'].append(self.buildToDict(build))
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
def badge(self, tenant, project=None, pipeline=None, branch=None):
connection = self._get_connection(tenant)
buildsets = connection.getBuildsets(
tenant=tenant, project=project, pipeline=pipeline,
branch=branch, limit=1)
if not buildsets:
raise cherrypy.HTTPError(404, 'No buildset found')
if buildsets[0].result == 'SUCCESS':
file = 'passing.svg'
else:
file = 'failing.svg'
path = os.path.join(self.zuulweb.static_path, file)
return cherrypy.lib.static.serve_file(
path=path, content_type="image/svg+xml")
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@ -1179,6 +1200,8 @@ class ZuulWeb(object):
controller=api, action='console_stream')
route_map.connect('api', '/api/tenant/{tenant}/builds',
controller=api, action='builds')
route_map.connect('api', '/api/tenant/{tenant}/badge',
controller=api, action='badge')
route_map.connect('api', '/api/tenant/{tenant}/build/{uuid}',
controller=api, action='build')
route_map.connect('api', '/api/tenant/{tenant}/buildsets',