diff --git a/doc/source/howtos/badges.rst b/doc/source/howtos/badges.rst
index e6613dfc81..10ff36c1d3 100644
--- a/doc/source/howtos/badges.rst
+++ b/doc/source/howtos/badges.rst
@@ -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 ``
`` 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``.
diff --git a/releasenotes/notes/dynamic-badges-9b0e7a39e73775ba.yaml b/releasenotes/notes/dynamic-badges-9b0e7a39e73775ba.yaml
new file mode 100644
index 0000000000..33a76488ef
--- /dev/null
+++ b/releasenotes/notes/dynamic-badges-9b0e7a39e73775ba.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Support for generating dynamic :ref:`badges` has been added.
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py
index af968b1bc9..381d4585a2 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -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('
diff --git a/web/public/openapi.yaml b/web/public/openapi.yaml
index 4338e5a43b..6313d1263a 100644
--- a/web/public/openapi.yaml
+++ b/web/public/openapi.yaml
@@ -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
diff --git a/web/public/passing.svg b/web/public/passing.svg
new file mode 100644
index 0000000000..54cf11b434
--- /dev/null
+++ b/web/public/passing.svg
@@ -0,0 +1,18 @@
+
diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py
index 6336ac15f5..9f5c504ca4 100755
--- a/zuul/web/__init__.py
+++ b/zuul/web/__init__.py
@@ -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',