Skip to content

Commit f9608f9

Browse files
committed
Add buildkite-webhook-handler lambda to ingest webhook events from Buildkite
This is used on vLLM CI for the time being Signed-off-by: Huy Do <huydhn@gmail.com>
1 parent bcc20e4 commit f9608f9

5 files changed

Lines changed: 274 additions & 4 deletions

File tree

.github/workflows/_lambda-do-release-runners.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ jobs:
8585
fail-fast: false
8686
matrix:
8787
include: [
88-
{ dir-name: 'ci-queue-pct', zip-name: 'ci-queue-pct' },
89-
{ dir-name: 'oss_ci_job_queue_time', zip-name: 'oss-ci-job-queue-time' },
90-
{ dir-name: 'oss_ci_cur', zip-name: 'oss-ci-cur' },
88+
{ dir-name: 'ci-queue-pct', zip-name: 'ci-queue-pct' },
89+
{ dir-name: 'oss_ci_job_queue_time', zip-name: 'oss-ci-job-queue-time' },
90+
{ dir-name: 'oss_ci_cur', zip-name: 'oss-ci-cur' },
9191
{ dir-name: 'benchmark-results-uploader', zip-name: 'benchmark-results-uploader' },
92-
{ dir-name: 'pytorch-auto-revert', zip-name: 'pytorch-auto-revert' },
92+
{ dir-name: 'pytorch-auto-revert', zip-name: 'pytorch-auto-revert' },
9393
{ dir-name: 'keep-going-call-log-classifier', zip-name: 'keep-going-call-log-classifier' },
94+
{ dir-name: 'buildkite-webhook-handler', zip-name: 'buildkite-webhook-handler' },
9495
]
9596
name: Upload Release for ${{ matrix.dir-name }} lambda
9697
runs-on: ubuntu-latest
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
all: run-local
2+
3+
clean:
4+
rm -rf deployment
5+
rm -rf venv
6+
rm -rf deployment.zip
7+
8+
venv/bin/python:
9+
virtualenv venv
10+
venv/bin/pip install -r requirements.txt
11+
12+
deployment.zip:
13+
mkdir -p deployment
14+
cp lambda_function.py ./deployment/.
15+
pip3.10 install -r requirements.txt -t ./deployment/. --platform manylinux2014_x86_64 --only-binary=:all: --implementation cp --python-version 3.10 --upgrade
16+
cd ./deployment && zip -q -r ../deployment.zip .
17+
18+
.PHONY: create-deployment-package
19+
create-deployment-package: deployment.zip
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Buildkite Webhook Handler Lambda
2+
3+
This Lambda function receives and processes Buildkite webhook events for build and job events, saving them to DynamoDB tables.
4+
5+
## Overview
6+
7+
The lambda handles two types of Buildkite webhook events:
8+
- **Agent events** (`agent.*`) - Saved to `vllm-buildkite-agent-events` table
9+
- **Build events** (`build.*`) - Saved to `vllm-buildkite-build-events` table
10+
- **Job events** (`job.*`) - Saved to `vllm-buildkite-job-events` table
11+
12+
## DynamoDB Schema
13+
14+
### Agent Events Table: `vllm-buildkite-agent-events`
15+
- **Partition Key**: `dynamoKey` (format: `AGENT_ID`)
16+
- https://buildkite.com/docs/apis/webhooks/pipelines/agent-events
17+
18+
### Build Events Table: `vllm-buildkite-build-events`
19+
- **Partition Key**: `dynamoKey` (format: `REPO_NAME/BUILD_NUMBER`)
20+
- https://buildkite.com/docs/apis/webhooks/pipelines/build-events
21+
22+
### Job Events Table: `vllm-buildkite-job-events`
23+
- **Partition Key**: `dynamoKey` (format: `REPO_NAME/JOB_ID`)
24+
- https://buildkite.com/docs/apis/webhooks/pipelines/job-events
25+
26+
## Deployment
27+
28+
```bash
29+
make create-deployment-package
30+
```
31+
32+
This creates a `deployment.zip` file ready for AWS Lambda deployment.
33+
34+
## Event Processing
35+
36+
The lambda automatically:
37+
1. Identifies event type from webhook payload
38+
2. Extracts repository name and relevant IDs
39+
3. Saves to appropriate DynamoDB table with structured key
40+
4. Returns success/error response
41+
42+
## Error Handling
43+
44+
- Invalid JSON payloads return 400 status
45+
- Missing required fields return 400 status
46+
- DynamoDB errors return 500 status
47+
- Unsupported event types return 400 status
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import json
2+
from typing import Any, Dict
3+
4+
import boto3
5+
from botocore.exceptions import ClientError
6+
7+
8+
dynamodb = boto3.resource("dynamodb")
9+
agent_events_table = dynamodb.Table("vllm-buildkite-agent-events")
10+
build_events_table = dynamodb.Table("vllm-buildkite-build-events")
11+
job_events_table = dynamodb.Table("vllm-buildkite-job-events")
12+
13+
14+
def save_agent_event(event_data: Dict[str, Any]) -> Dict[str, Any]:
15+
"""
16+
Save agent events to DynamoDB table.
17+
18+
Args:
19+
event_data: The agent event payload from Buildkite
20+
21+
Returns:
22+
Dict[str, Any]: Response containing status and result information
23+
"""
24+
try:
25+
agent = event_data.get("agent", {})
26+
agent_id = agent.get("id", "")
27+
28+
if not agent_id:
29+
return {
30+
"statusCode": 400,
31+
"body": json.dumps({"message": "Missing agent ID"}),
32+
}
33+
34+
dynamo_key = agent_id
35+
item = {"dynamoKey": dynamo_key, **event_data}
36+
37+
agent_events_table.put_item(Item=item)
38+
39+
return {
40+
"statusCode": 200,
41+
"body": json.dumps(
42+
{"message": f"Agent event saved successfully with key: {dynamo_key}"}
43+
),
44+
}
45+
46+
except ClientError as e:
47+
return {
48+
"statusCode": 500,
49+
"body": json.dumps({"message": f"DynamoDB error: {str(e)}"}),
50+
}
51+
except Exception as e:
52+
return {
53+
"statusCode": 500,
54+
"body": json.dumps({"message": f"Error saving agent event: {str(e)}"}),
55+
}
56+
57+
58+
def save_build_event(event_data: Dict[str, Any]) -> Dict[str, Any]:
59+
"""
60+
Save build event to DynamoDB table.
61+
62+
Args:
63+
event_data: The build event payload from Buildkite
64+
65+
Returns:
66+
Dict[str, Any]: Response containing status and result information
67+
"""
68+
try:
69+
build = event_data.get("build", {})
70+
repo_name = event_data.get("pipeline", {}).get("repository", "").split("/")[-1]
71+
build_number = build.get("number", "")
72+
73+
if not repo_name or not build_number:
74+
return {
75+
"statusCode": 400,
76+
"body": json.dumps(
77+
{"message": "Missing repository name or build number"}
78+
),
79+
}
80+
81+
dynamo_key = f"{repo_name}/{build_number}"
82+
83+
item = {"dynamoKey": dynamo_key, **event_data}
84+
85+
build_events_table.put_item(Item=item)
86+
87+
return {
88+
"statusCode": 200,
89+
"body": json.dumps(
90+
{"message": f"Build event saved successfully with key: {dynamo_key}"}
91+
),
92+
}
93+
94+
except ClientError as e:
95+
return {
96+
"statusCode": 500,
97+
"body": json.dumps({"message": f"DynamoDB error: {str(e)}"}),
98+
}
99+
except Exception as e:
100+
return {
101+
"statusCode": 500,
102+
"body": json.dumps({"message": f"Error saving build event: {str(e)}"}),
103+
}
104+
105+
106+
def save_job_event(event_data: Dict[str, Any]) -> Dict[str, Any]:
107+
"""
108+
Save job event to DynamoDB table.
109+
110+
Args:
111+
event_data: The job event payload from Buildkite
112+
113+
Returns:
114+
Dict[str, Any]: Response containing status and result information
115+
"""
116+
try:
117+
job = event_data.get("job", {})
118+
repo_name = event_data.get("pipeline", {}).get("repository", "").split("/")[-1]
119+
job_id = job.get("id", "")
120+
121+
if not repo_name or not job_id:
122+
return {
123+
"statusCode": 400,
124+
"body": json.dumps({"message": "Missing repository name or job ID"}),
125+
}
126+
127+
dynamo_key = f"{repo_name}/{job_id}"
128+
129+
item = {"dynamoKey": dynamo_key, **event_data}
130+
131+
job_events_table.put_item(Item=item)
132+
133+
return {
134+
"statusCode": 200,
135+
"body": json.dumps(
136+
{"message": f"Job event saved successfully with key: {dynamo_key}"}
137+
),
138+
}
139+
140+
except ClientError as e:
141+
return {
142+
"statusCode": 500,
143+
"body": json.dumps({"message": f"DynamoDB error: {str(e)}"}),
144+
}
145+
except Exception as e:
146+
return {
147+
"statusCode": 500,
148+
"body": json.dumps({"message": f"Error saving job event: {str(e)}"}),
149+
}
150+
151+
152+
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
153+
"""
154+
Main Lambda handler function for Buildkite webhook events.
155+
156+
Args:
157+
event: Contains the webhook payload from Buildkite
158+
context: Provides runtime information about the Lambda function
159+
160+
Returns:
161+
Dict[str, Any]: Response containing status and result information
162+
"""
163+
try:
164+
if event.get("body"):
165+
body = json.loads(event["body"])
166+
else:
167+
body = event
168+
169+
event_type = body.get("event")
170+
171+
if not event_type:
172+
return {
173+
"statusCode": 400,
174+
"body": json.dumps(
175+
{"message": "Missing event type in webhook payload"}
176+
),
177+
}
178+
179+
if event_type.startswith("agent."):
180+
return save_agent_event(body)
181+
elif event_type.startswith("build."):
182+
return save_build_event(body)
183+
elif event_type.startswith("job."):
184+
return save_job_event(body)
185+
else:
186+
return {
187+
"statusCode": 400,
188+
"body": json.dumps(
189+
{"message": f"Unsupported event type: {event_type}"}
190+
),
191+
}
192+
193+
except json.JSONDecodeError as e:
194+
return {
195+
"statusCode": 400,
196+
"body": json.dumps({"message": f"Invalid JSON payload: {str(e)}"}),
197+
}
198+
except Exception as e:
199+
return {
200+
"statusCode": 500,
201+
"body": json.dumps({"message": f"Unexpected error: {str(e)}"}),
202+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
boto3==1.36.21

0 commit comments

Comments
 (0)