You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/mcp/buildbucket.py

325 lines
12 KiB
Python

# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tools for interacting with buildbucket"""
import json
import urllib.parse
from mcp.server import fastmcp
import telemetry
import common
BUILDBUCKET_SERVER = 'cr-buildbucket.appspot.com'
tracer = telemetry.get_tracer(__name__)
async def get_build_status(
ctx: fastmcp.Context,
build_id: str,
) -> str:
"""Gets the build status from the provided build_id
Args:
build_id: The buildbucket id of the build. This is not the build number.
Return:
The status of the build as a string
"""
with tracer.start_as_current_span('chromium.mcp.get_build_status'):
await ctx.info(f'Received request {build_id}')
request = {'id': build_id}
response = await common.run_prpc_call(
ctx, BUILDBUCKET_SERVER, 'buildbucket.v2.Builds.GetBuildStatus',
request)
return json.loads(response)['status']
async def get_build_from_id(
ctx: fastmcp.Context,
build_id: str,
fields: list[str],
) -> str:
"""Gets a buildbucket build from its ID
The url of a build can be deconstructed and used to get more details about
the build. e.g.
https://ci.chromium.org/b/<build_id>
Args:
build_id: The request body for the RPC. All fields should be represented
by strings. Integer fields will be parsed later.
https://chromium.googlesource.com/infra/luci/luci-go/+/main/buildbucket/proto/builds_service.proto
for more details.
The request's mask can be set to get more information. By default only
high level statuses will be returned. Some useful fields to include in
this mask are:
status, input, output, id, builder, builder_info, tags, steps, infra
Multiple fields in the mask can be included as a comma separated string
e.g.
The build_number is mutually exclusive with the build_id. To get the
build from a build_id, only the build_id is needed. e.g.
{
"id": "<build id>",
"mask": {
"fields": "steps,tags"
}
}
fields: A list of fields to return. Options are:
status, input, output, id, builder, builder_info, tags, steps, infra
Returns:
The build in json format including the requested fields. See:
https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
"""
with tracer.start_as_current_span('chromium.mcp.get_build_from_id'):
request = {'id': build_id, 'mask': {'fields': ','.join(fields)}}
response = await common.run_prpc_call(ctx, BUILDBUCKET_SERVER,
'buildbucket.v2.Builds.GetBuild',
request)
return response
async def get_build_from_build_number(
ctx: fastmcp.Context,
build_number: int,
builder_name: int,
builder_bucket: int,
builder_project: int,
fields: list[str],
) -> str:
"""Gets a buildbucket build from its build number and builder
The url of a build can be deconstructed and used to get more details about
the build. e.g.
https://ci.chromium.org/b/<build_id>
Args:
build_id: The request body for the RPC. All fields should be represented
by strings. Integer fields will be parsed later.
https://chromium.googlesource.com/infra/luci/luci-go/+/main/buildbucket/proto/builds_service.proto
for more details.
The request's mask can be set to get more information. By default only
high level statuses will be returned. Some useful fields to include in
this mask are:
status, input, output, id, builder, builder_info, tags, steps, infra
Multiple fields in the mask can be included as a comma separated string
The build_number is mutually exclusive with the build_id. To get the
build from a build_id only the build_id is needed. e.g.
{
"id": "<build_id>",
"mask": {
"fields": "steps,tags"
}
}
fields: A list of fields to return. Options are:
status, input, output, id, builder, builder_info, tags, steps, infra
Returns:
The build in json format including the requested fields. See
https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
"""
with tracer.start_as_current_span(
'chromium.mcp.get_build_from_build_number'):
request = {
'buildNumber': build_number,
'builder': {
'builder': builder_name,
'bucket': builder_bucket,
'project': builder_project
},
'mask': {
'fields': ','.join(fields)
}
}
response = await common.run_prpc_call(ctx, BUILDBUCKET_SERVER,
'buildbucket.v2.Builds.GetBuild',
request)
return response
async def get_build(
ctx: fastmcp.Context,
request: dict,
) -> str:
"""Calls the buildbucket.v2.Builds.GetBuild RPC to fetch a build
The url of a build can be deconstructed and used to get more details about
the
build. e.g.
https://ci.chromium.org/ui/p/chromium/builders/<bucket>/<builder_name>/<build_number>/infra
Builds can also be in the form:
https://ci.chromium.org/b/<build id>
Args:
request: The request body for the RPC. All fields should be represented
by strings. Integer fields will be parsed later.
https://chromium.googlesource.com/infra/luci/luci-go/+/main/buildbucket/proto/builds_service.proto
for more details.
The request's mask can be set to get more information. By default only
high level statuses will be returned. Some useful fields to include in
this mask are:
status, input, output, id, builder, builder_info, tags, steps, infra
Multiple fields in the mask can be included as a comma separated string
e.g.
{
"build_number": "<build number>",
"builder": {
"bucket": "<bucket>",
"builder": "<builder name>",
"project": "chromium"
},
"mask": {
"fields": "steps,tags"
}
}
The build_number is mutually exclusive with the build_id. To get the
build from a build_id, only the build_id is needed. e.g.
{
"id": "<build id>",
"mask": {
"fields": "steps,tags"
}
}
Returns:
The stdout of the prpc command which should be a JSON string for a
buildbucket.v2.Build proto. See
https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_proto/go.chromium.org/luci/buildbucket/proto/build.proto
for more details.
"""
with tracer.start_as_current_span('chromium.mcp.get_build'):
await ctx.info(f'Received request {request}')
response = await common.run_prpc_call(ctx, BUILDBUCKET_SERVER,
'buildbucket.v2.Builds.GetBuild',
request)
return response
async def get_recent_builds(
ctx: fastmcp.Context,
builder_name: str,
builder_bucket: str,
builder_project: str,
num_builds: int,
) -> str:
"""Gets |num_builds| recent completed builds for a builder.
This will consider any builds that have run to completion, regardless of
status.
The url of a builder can be deconstructed to get the relevant information,
e.g.
https://ci.chromium.org/ui/p/<project>/builders/<bucket>/<name>
Args:
builder_name: The name of the builder to get builds from. Any URL
encoding will automatically be decoded.
builder_bucket: The bucket the builder belongs to.
builder_project: The project the builder belongs to.
num_builds: How many builds to retrieve. Per the proto definition for
the underlying request, values >1000 will be treated as 1000.
Returns:
The stdout of the prpc command which should be a JSON string for a
buildbucket.v2.SearchBuildsResponse proto. See
https://source.chromium.org/chromium/infra/infra_superproject/+/main:infra/go/src/go.chromium.org/luci/buildbucket/proto/builds_service.proto
for more details.
"""
with tracer.start_as_current_span('chromium.mcp.get_recent_builds'):
return await _get_recent_builds(
ctx,
builder_name,
builder_bucket,
builder_project,
num_builds,
failed_builds_only=False,
)
async def get_recent_failed_builds(
ctx: fastmcp.Context,
builder_name: str,
builder_bucket: str,
builder_project: str,
num_builds: int,
) -> str:
"""Gets |num_builds| recent failed builds for a builder.
This will only consider builds that have run to completion and exited with
the FAILURE status, i.e. builds that show up as red in Milo.
The url of a builder can be deconstructed to get the relevant information,
e.g.
https://ci.chromium.org/ui/p/<project>/builders/<bucket>/<name>
Args:
builder_name: The name of the builder to get builds from. Any URL
encoding will automatically be decoded.
builder_bucket: The bucket the builder belongs to.
builder_project: The project the builder belongs to.
num_builds: How many builds to retrieve. Per the proto definition for
the underlying request, values >1000 will be treated as 1000.
Returns:
The stdout of the prpc command which should be a JSON string for a
buildbucket.v2.SearchBuildsResponse proto. See
https://source.chromium.org/chromium/infra/infra_superproject/+/main:infra/go/src/go.chromium.org/luci/buildbucket/proto/builds_service.proto
for more details.
"""
with tracer.start_as_current_span('chromium.mcp.get_recent_failed_builds'):
return await _get_recent_builds(
ctx,
builder_name,
builder_bucket,
builder_project,
num_builds,
failed_builds_only=True,
)
async def _get_recent_builds(
ctx: fastmcp.Context,
builder_name: str,
builder_bucket: str,
builder_project: str,
num_builds: int,
failed_builds_only: bool,
) -> str:
"""Helper function to get recent builds for a builder.
See docstrings for get_recent_builds/get_recent_failed_builds for more
information.
Args:
builder_name: Same as caller.
builder_bucket: Same as caller.
builder_project: Same as caller.
num_builds: Same as caller.
failed_builds_only: Whether to only search for failed builds instead of
all completed builds.
Returns:
Same as caller.
"""
if num_builds < 1:
raise ValueError(f'Provided num_builds {num_builds} is not positive')
request = {
'predicate': {
'builder': {
'project': builder_project,
'bucket': builder_bucket,
'builder': urllib.parse.unquote(builder_name),
},
'status': 'FAILURE' if failed_builds_only else 'ENDED_MASK',
},
'page_size': f'{num_builds}'
}
response = await common.run_prpc_call(ctx, BUILDBUCKET_SERVER,
'buildbucket.v2.Builds.SearchBuilds',
request)
return response