From 9cf2345aa90d411c09d954fc042488bfebad6e9a Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Mon, 15 Jan 2024 21:16:38 -0600 Subject: [PATCH] jenkins: Add Jenkins webhook Using the [Generic Event Plugin][0], we can receive a notification from Jenkins when builds start and finish. We'll relay these to *ntfy* on a unique topic that I will subscribe to on my desktop. That way, I can get desktop notifications about jobs while I am working, which will be particularly useful while developing and troubleshooting pipelines. [0]: https://plugins.jenkins.io/generic-event/ --- dch_webhooks.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/dch_webhooks.py b/dch_webhooks.py index d5e9b42..69816b0 100644 --- a/dch_webhooks.py +++ b/dch_webhooks.py @@ -1,3 +1,4 @@ +import base64 import datetime import importlib.metadata import logging @@ -44,6 +45,7 @@ MAX_DOCUMENT_SIZE = int( 50 * 2**20, ) ) +NTFY_URL = os.environ.get('NTFY_URL', 'http://ntfy.ntfy:2586') PAPERLESS_URL = os.environ.get( 'PAPERLESS_URL', 'http://paperless-ngx', @@ -308,6 +310,55 @@ def response_filename(response: httpx.Response) -> str: return response.url.path.rstrip('/').rsplit('/', 1)[-1] +async def ntfy( + message: Optional[str], + topic: str, + title: Optional[str] = None, + tags: Optional[str] = None, + attach: Optional[bytes] = None, + filename: Optional[str] = None, +) -> None: + assert message or attach + headers = { + } + if title: + headers['Title'] = title + if tags: + headers['Tags'] = tags + url = f'{NTFY_URL}/{topic}' + client = httpx.AsyncClient() + if attach: + if filename: + headers['Filename'] = filename + if message: + try: + message.encode("ascii") + except UnicodeEncodeError: + message = rfc2047_base64encode(message) + else: + message = message.replace('\n', '\\n') + headers['Message'] = message + r = await client.put( + url, + headers=headers, + content=attach, + ) + else: + r = await client.post( + url, + headers=headers, + content=message, + ) + r.raise_for_status() + + +def rfc2047_base64encode( + message: str, +) -> str: + encoded = base64.b64encode(message.encode("utf-8")).decode("ascii") + return f"=?UTF-8?B?{encoded}?=" + + app = fastapi.FastAPI( name=DIST['Name'], version=DIST['Version'], @@ -331,3 +382,46 @@ def status() -> str: @app.post('/hooks/firefly-iii/create') async def firefly_iii_create(hook: FireflyIIIWebhook) -> None: await handle_firefly_transaction(hook.content) + + +@app.post('/hooks/jenkins') +async def jenkins_notify(request: fastapi.Request) -> None: + body = await request.json() + data = body.get('data', {}) + if body['type'] == 'run.started': + title = 'Build started' + tag = 'building_construction' + build_cause = None + for action in data.get('actions', []): + for cause in action.get('causes', []): + if build_cause := cause.get('shortDescription'): + break + else: + continue + break + message = f'Build started: {data["fullDisplayName"]}' + if build_cause: + message = f'{message} ({build_cause})' + elif body['type'] == 'run.finalized': + message = f'{data["fullDisplayName"]} {data["result"]}' + title = 'Build finished' + match data['result']: + case 'FAILURE': + tag = 'red_circle' + case 'SUCCESS': + tag = 'green_circle' + case 'UNSTABLE': + tag = 'yellow_circle' + case 'NOT_BUILT': + tag = 'large_blue_circle' + case 'ABORTED': + tag = 'black_circle' + case _: + tag = 'white_circle' + else: + return + + try: + await ntfy(message, 'jenkins', title, tag) + except httpx.HTTPError: + log.exception('Failed to send notification:')