Skip to content

Commit 0940b14

Browse files
committed
Log the X-GitHub-Delivery Header
1 parent ad3fc60 commit 0940b14

2 files changed

Lines changed: 230 additions & 18 deletions

File tree

app/routes/webhook.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ def webhook():
1818
"""Handle incoming GitHub webhook events."""
1919
# https://docs.github.com/en/webhooks/webhook-events-and-payloads
2020
event_type = request.headers.get('X-GitHub-Event')
21+
# https://docs.github.com/en/webhooks/webhook-events-and-payloads#delivery-headers
22+
delivery_id = request.headers.get('X-GitHub-Delivery')
2123

2224
# Validate event type
2325
if not event_type or not isinstance(event_type, str):
2426
logger.error("Missing or invalid X-GitHub-Event header")
2527
return jsonify({'status': 'error', 'message': 'Invalid event type'}), 400
2628

27-
logger.info("Received webhook event: %s", event_type)
29+
logger.info("Received webhook event: %s, delivery_id: %s", event_type, delivery_id)
2830

2931
# Handle ping event
3032
if event_type == 'ping':
@@ -34,32 +36,51 @@ def webhook():
3436
signature = request.headers.get('X-Hub-Signature-256')
3537

3638
if not verify_github_signature(request.data, signature):
37-
logger.error("GitHub webhook signature not successfully verified! Ignoring webhook event.")
39+
logger.error(
40+
"GitHub webhook signature not successfully verified! "
41+
"Ignoring webhook event. delivery_id: %s",
42+
delivery_id,
43+
)
3844
return jsonify({'status': 'forbidden', 'message': 'Invalid signature'}), 403
3945

4046
# Validate JSON payload
4147
try:
4248
payload = request.json
4349
if not payload:
44-
logger.error("Empty or invalid JSON payload")
50+
logger.error("Empty or invalid JSON payload, delivery_id: %s", delivery_id)
4551
return jsonify({'status': 'error', 'message': 'Invalid JSON payload'}), 400
4652
except Exception as e:
47-
logger.error("Failed to parse JSON payload: %s", str(e))
53+
logger.error(
54+
"Failed to parse JSON payload: %s, delivery_id: %s",
55+
str(e),
56+
delivery_id,
57+
)
4858
return jsonify({'status': 'error', 'message': 'Invalid JSON'}), 400
4959

5060
# https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_job
5161
if event_type == 'workflow_job':
52-
return handle_workflow_job_event(payload)
62+
return handle_workflow_job_event(payload, delivery_id)
5363
else:
54-
logger.warning("Received unknown event type: %s", event_type)
64+
logger.warning(
65+
"Received unknown event type: %s, delivery_id: %s",
66+
event_type,
67+
delivery_id,
68+
)
5569
return jsonify({'status': 'ignored'}), 200
5670

5771

58-
def handle_workflow_job_event(payload):
72+
def handle_workflow_job_event(payload, delivery_id=None):
5973
"""Handle workflow_job event."""
6074
try:
6175
webhook_service = WebhookService()
6276
result = webhook_service.handle_workflow_job(payload)
77+
logger.info(
78+
"Webhook processed successfully, action: %s, runner_name: %s, "
79+
"delivery_id: %s",
80+
result.get("action"),
81+
result.get("runner_name"),
82+
delivery_id,
83+
)
6384
return (
6485
jsonify(
6586
{
@@ -71,8 +92,16 @@ def handle_workflow_job_event(payload):
7192
200,
7293
)
7394
except ValueError as e:
74-
logger.error("[Webhook] Validation error: %s", str(e))
95+
logger.error(
96+
"[Webhook] Validation error: %s, delivery_id: %s",
97+
str(e),
98+
delivery_id,
99+
)
75100
return jsonify({'status': 'error', 'message': 'Invalid payload'}), 400
76101
except Exception as e:
77-
logger.error("[Webhook] Error handling webhook: %s", str(e))
102+
logger.error(
103+
"[Webhook] Error handling webhook: %s, delivery_id: %s",
104+
str(e),
105+
delivery_id,
106+
)
78107
return jsonify({'status': 'error', 'message': 'Internal error'}), 500

tests/unit/test_routes_webhook.py

Lines changed: 192 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
from unittest.mock import patch
34

45

@@ -18,7 +19,10 @@ def test_workflow_job_webhook(self, mock_webhook_service, mock_verify, client, s
1819
'/webhook',
1920
data=json.dumps(sample_workflow_job_payload),
2021
content_type='application/json',
21-
headers={'X-GitHub-Event': 'workflow_job'}
22+
headers={
23+
'X-GitHub-Event': 'workflow_job',
24+
'X-GitHub-Delivery': 'abc-123-def'
25+
}
2226
)
2327

2428
assert response.status_code == 200
@@ -51,7 +55,10 @@ def test_workflow_job_webhook_created_response(self, mock_webhook_service, mock_
5155
'/webhook',
5256
data=json.dumps(payload),
5357
content_type='application/json',
54-
headers={'X-GitHub-Event': 'workflow_job'}
58+
headers={
59+
'X-GitHub-Event': 'workflow_job',
60+
'X-GitHub-Delivery': 'delivery-created-001'
61+
}
5562
)
5663

5764
assert response.status_code == 200
@@ -79,7 +86,10 @@ def test_workflow_job_webhook_deleted_response(self, mock_webhook_service, mock_
7986
'/webhook',
8087
data=json.dumps(payload),
8188
content_type='application/json',
82-
headers={'X-GitHub-Event': 'workflow_job'}
89+
headers={
90+
'X-GitHub-Event': 'workflow_job',
91+
'X-GitHub-Delivery': 'delivery-deleted-001'
92+
}
8393
)
8494

8595
assert response.status_code == 200
@@ -111,7 +121,10 @@ def test_workflow_job_webhook_ignored_response(self, mock_webhook_service, mock_
111121
'/webhook',
112122
data=json.dumps(payload),
113123
content_type='application/json',
114-
headers={'X-GitHub-Event': 'workflow_job'}
124+
headers={
125+
'X-GitHub-Event': 'workflow_job',
126+
'X-GitHub-Delivery': 'delivery-ignored-001'
127+
}
115128
)
116129

117130
assert response.status_code == 200
@@ -128,7 +141,10 @@ def test_installation_webhook(self, mock_verify, client, sample_installation_pay
128141
'/webhook',
129142
data=json.dumps(sample_installation_payload),
130143
content_type='application/json',
131-
headers={'X-GitHub-Event': 'installation'}
144+
headers={
145+
'X-GitHub-Event': 'installation',
146+
'X-GitHub-Delivery': 'delivery-install-001'
147+
}
132148
)
133149

134150
assert response.status_code == 200
@@ -144,7 +160,10 @@ def test_unknown_webhook_event(self, mock_verify, client):
144160
'/webhook',
145161
data=json.dumps(payload),
146162
content_type='application/json',
147-
headers={'X-GitHub-Event': 'unknown_event'}
163+
headers={
164+
'X-GitHub-Event': 'unknown_event',
165+
'X-GitHub-Delivery': 'delivery-unknown-001'
166+
}
148167
)
149168

150169
assert response.status_code == 200
@@ -162,7 +181,10 @@ def test_workflow_job_webhook_error(self, mock_webhook_service, mock_verify, cli
162181
'/webhook',
163182
data=json.dumps(sample_workflow_job_payload),
164183
content_type='application/json',
165-
headers={'X-GitHub-Event': 'workflow_job'}
184+
headers={
185+
'X-GitHub-Event': 'workflow_job',
186+
'X-GitHub-Delivery': 'delivery-error-001'
187+
}
166188
)
167189

168190
assert response.status_code == 500
@@ -181,7 +203,8 @@ def test_webhook_invalid_signature(self, mock_verify, client):
181203
content_type='application/json',
182204
headers={
183205
'X-GitHub-Event': 'workflow_job',
184-
'X-Hub-Signature-256': 'invalid'
206+
'X-Hub-Signature-256': 'invalid',
207+
'X-GitHub-Delivery': 'delivery-sig-001'
185208
}
186209
)
187210

@@ -194,8 +217,168 @@ def test_webhook_ping_event(self, client):
194217
'/webhook',
195218
data=json.dumps({'zen': 'test'}),
196219
content_type='application/json',
197-
headers={'X-GitHub-Event': 'ping'}
220+
headers={
221+
'X-GitHub-Event': 'ping',
222+
'X-GitHub-Delivery': 'delivery-ping-001'
223+
}
198224
)
199225

200226
assert response.status_code == 200
201227
assert response.json['status'] == 'success'
228+
229+
230+
class TestWebhookDeliveryIdLogging:
231+
"""Tests to verify that X-GitHub-Delivery header is logged for correlation."""
232+
233+
@patch('app.routes.webhook.verify_github_signature')
234+
@patch('app.routes.webhook.WebhookService')
235+
def test_delivery_id_logged_on_workflow_job(
236+
self, mock_webhook_service, mock_verify, client, caplog
237+
):
238+
"""Test that delivery_id is logged when processing a workflow_job event."""
239+
mock_verify.return_value = True
240+
mock_service_instance = mock_webhook_service.return_value
241+
mock_service_instance.handle_workflow_job.return_value = {
242+
"action": "created",
243+
"runner_name": "runner-abc123"
244+
}
245+
246+
payload = {
247+
'action': 'queued',
248+
'workflow_job': {'labels': ['gcp-ubuntu-24.04']},
249+
'repository': {
250+
'html_url': 'https://github.com/owner/repo',
251+
'full_name': 'owner/repo'
252+
}
253+
}
254+
255+
with caplog.at_level(logging.INFO, logger='app.routes.webhook'):
256+
client.post(
257+
'/webhook',
258+
data=json.dumps(payload),
259+
content_type='application/json',
260+
headers={
261+
'X-GitHub-Event': 'workflow_job',
262+
'X-GitHub-Delivery': 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
263+
}
264+
)
265+
266+
assert any(
267+
'f47ac10b-58cc-4372-a567-0e02b2c3d479' in record.message
268+
for record in caplog.records
269+
), "delivery_id was not found in log output"
270+
271+
@patch('app.routes.webhook.verify_github_signature')
272+
def test_delivery_id_logged_on_unknown_event(self, mock_verify, client, caplog):
273+
"""Test that delivery_id is logged for unknown event types."""
274+
mock_verify.return_value = True
275+
276+
with caplog.at_level(logging.WARNING, logger='app.routes.webhook'):
277+
client.post(
278+
'/webhook',
279+
data=json.dumps({'action': 'test'}),
280+
content_type='application/json',
281+
headers={
282+
'X-GitHub-Event': 'unknown_event',
283+
'X-GitHub-Delivery': 'aaaabbbb-cccc-dddd-eeee-ffffffffffff'
284+
}
285+
)
286+
287+
assert any(
288+
'aaaabbbb-cccc-dddd-eeee-ffffffffffff' in record.message
289+
for record in caplog.records
290+
), "delivery_id was not found in log output for unknown event"
291+
292+
@patch('app.routes.webhook.verify_github_signature')
293+
def test_delivery_id_logged_on_invalid_signature(self, mock_verify, client, caplog):
294+
"""Test that delivery_id is logged when signature verification fails."""
295+
mock_verify.return_value = False
296+
297+
with caplog.at_level(logging.ERROR, logger='app.routes.webhook'):
298+
client.post(
299+
'/webhook',
300+
data=json.dumps({'test': 'data'}),
301+
content_type='application/json',
302+
headers={
303+
'X-GitHub-Event': 'workflow_job',
304+
'X-Hub-Signature-256': 'invalid',
305+
'X-GitHub-Delivery': '11112222-3333-4444-5555-666677778888'
306+
}
307+
)
308+
309+
assert any(
310+
'11112222-3333-4444-5555-666677778888' in record.message
311+
for record in caplog.records
312+
), "delivery_id was not found in log output for invalid signature"
313+
314+
@patch('app.routes.webhook.verify_github_signature')
315+
@patch('app.routes.webhook.WebhookService')
316+
def test_delivery_id_logged_on_webhook_error(
317+
self, mock_webhook_service, mock_verify, client, caplog
318+
):
319+
"""Test that delivery_id is logged when webhook processing fails."""
320+
mock_verify.return_value = True
321+
mock_service_instance = mock_webhook_service.return_value
322+
mock_service_instance.handle_workflow_job.side_effect = Exception("boom")
323+
324+
payload = {
325+
'action': 'queued',
326+
'workflow_job': {'labels': ['gcp-ubuntu-24.04']},
327+
'repository': {
328+
'html_url': 'https://github.com/owner/repo',
329+
'full_name': 'owner/repo'
330+
}
331+
}
332+
333+
with caplog.at_level(logging.ERROR, logger='app.routes.webhook'):
334+
client.post(
335+
'/webhook',
336+
data=json.dumps(payload),
337+
content_type='application/json',
338+
headers={
339+
'X-GitHub-Event': 'workflow_job',
340+
'X-GitHub-Delivery': 'error-delivery-id-999'
341+
}
342+
)
343+
344+
assert any(
345+
'error-delivery-id-999' in record.message
346+
for record in caplog.records
347+
), "delivery_id was not found in error log output"
348+
349+
@patch('app.routes.webhook.verify_github_signature')
350+
@patch('app.routes.webhook.WebhookService')
351+
def test_delivery_id_none_when_header_missing(
352+
self, mock_webhook_service, mock_verify, client, caplog
353+
):
354+
"""Test that delivery_id is logged as None when header is missing."""
355+
mock_verify.return_value = True
356+
mock_service_instance = mock_webhook_service.return_value
357+
mock_service_instance.handle_workflow_job.return_value = {
358+
"action": "created",
359+
"runner_name": "runner-no-delivery"
360+
}
361+
362+
payload = {
363+
'action': 'queued',
364+
'workflow_job': {'labels': ['gcp-ubuntu-24.04']},
365+
'repository': {
366+
'html_url': 'https://github.com/owner/repo',
367+
'full_name': 'owner/repo'
368+
}
369+
}
370+
371+
with caplog.at_level(logging.INFO, logger='app.routes.webhook'):
372+
response = client.post(
373+
'/webhook',
374+
data=json.dumps(payload),
375+
content_type='application/json',
376+
headers={'X-GitHub-Event': 'workflow_job'}
377+
)
378+
379+
assert response.status_code == 200
380+
# delivery_id should be None in the log since header was not sent
381+
assert any(
382+
'delivery_id: None' in record.message
383+
for record in caplog.records
384+
), "delivery_id: None was not found in log output when header is missing"

0 commit comments

Comments
 (0)