go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/services/buildbucket.ts (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import stableStringify from 'fast-json-stable-stringify'; 16 17 import { 18 GerritChange, 19 GitilesCommit, 20 StringPair, 21 } from '@/common/services/common'; 22 import { cached, CacheOption } from '@/generic_libs/tools/cached_fn'; 23 import { PrpcClientExt } from '@/generic_libs/tools/prpc_client_ext'; 24 25 /* eslint-disable max-len */ 26 /** 27 * Manually coded type definition and classes for buildbucket services. 28 * TODO(weiweilin): To be replaced by code generated version once we have one. 29 * source: https://chromium.googlesource.com/infra/luci/luci-go/+/04a118946d13ad326c44dba9a635116ff7f31c4e/buildbucket/proto/builds_service.proto 30 * Builder metadata source: https://chromium.googlesource.com/infra/luci/luci-go/+/fe56f864b0e1dc61eaa6b9062fabb1119e872306/buildbucket/proto/builder_service.proto 31 */ 32 /* eslint-enable max-len */ 33 34 export const PERM_BUILDS_CANCEL = 'buildbucket.builds.cancel'; 35 export const PERM_BUILDS_ADD = 'buildbucket.builds.add'; 36 export const PERM_BUILDS_GET = 'buildbucket.builds.get'; 37 export const PERM_BUILDS_GET_LIMITED = 'buildbucket.builds.getLimited'; 38 39 export const TEST_PRESENTATION_KEY = 40 '$recipe_engine/resultdb/test_presentation'; 41 export const BLAMELIST_PIN_KEY = '$recipe_engine/milo/blamelist_pins'; 42 43 export const BUILD_FIELD_MASK = 44 'id,builder,builderInfo,number,canceledBy,' + 45 'createTime,startTime,endTime,cancelTime,status,statusDetails,summaryMarkdown,input,output,steps,' + 46 'infra.buildbucket.agent,infra.swarming,infra.resultdb,infra.backend,' + 47 'tags,exe,schedulingTimeout,executionTimeout,gracePeriod,ancestorIds,retriable'; 48 49 // Includes: id, builder, number, createTime, startTime, endTime, status, summaryMarkdown. 50 export const SEARCH_BUILD_FIELD_MASK = 51 'builds.*.id,builds.*.builder,builds.*.number,builds.*.createTime,builds.*.startTime,builds.*.endTime,' + 52 'builds.*.status,builds.*.summaryMarkdown'; 53 54 export const enum Trinary { 55 Unset = 'UNSET', 56 Yes = 'YES', 57 No = 'NO', 58 } 59 60 export interface TimeRange { 61 readonly startTime?: string; 62 readonly endTime?: string; 63 } 64 65 export interface GetBuildRequest { 66 readonly id?: string; 67 readonly builder?: BuilderID; 68 readonly buildNumber?: number; 69 readonly fields?: string; 70 } 71 72 export interface SearchBuildsRequest { 73 readonly predicate: BuildPredicate; 74 readonly pageSize?: number; 75 readonly pageToken?: string; 76 readonly fields?: string; 77 } 78 79 export interface SearchBuildsResponse { 80 readonly builds?: readonly Build[]; 81 readonly nextPageToken?: string; 82 } 83 84 export interface BuildPredicate { 85 readonly builder?: BuilderID; 86 readonly status?: BuildbucketStatus | BuildStatusMask; 87 readonly gerritChanges?: readonly GerritChange[]; 88 readonly createdBy?: string; 89 readonly tags?: readonly StringPair[]; 90 readonly build?: BuildRange; 91 readonly experiments?: readonly string[]; 92 readonly includeExperimental?: boolean; 93 readonly createTime?: TimeRange; 94 } 95 96 export interface BuildRange { 97 readonly startBuildId: string; 98 readonly endBuildId: string; 99 } 100 101 export interface BuilderID { 102 readonly project: string; 103 readonly bucket: string; 104 readonly builder: string; 105 } 106 107 export enum BuilderMask { 108 CONFIG_ONLY = 'CONFIG_ONLY', 109 ALL = 'ALL', 110 METADATA_ONLY = 'METADATA_ONLY', 111 } 112 113 export interface Timestamp { 114 readonly seconds: number; 115 readonly nanos: number; 116 } 117 118 export interface Build { 119 readonly id: string; 120 readonly builder: BuilderID; 121 readonly builderInfo?: { 122 readonly description?: string; 123 }; 124 readonly number?: number; 125 readonly canceledBy?: string; 126 readonly createTime: string; 127 readonly startTime?: string; 128 readonly endTime?: string; 129 readonly cancelTime?: string; 130 readonly status: BuildbucketStatus; 131 readonly statusDetails?: StatusDetails; 132 readonly summaryMarkdown?: string; 133 readonly input?: BuildInput; 134 readonly output?: BuildOutput; 135 readonly steps?: readonly Step[]; 136 readonly infra?: BuildInfra; 137 readonly tags?: readonly StringPair[]; 138 readonly exe?: Executable; 139 readonly schedulingTimeout?: string; 140 readonly executionTimeout?: string; 141 readonly gracePeriod?: string; 142 readonly ancestorIds?: string[]; 143 readonly retriable?: Trinary; 144 } 145 146 // This is from https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/buildbucket/proto/common.proto#25 147 export enum BuildbucketStatus { 148 Scheduled = 'SCHEDULED', 149 Started = 'STARTED', 150 Success = 'SUCCESS', 151 Failure = 'FAILURE', 152 InfraFailure = 'INFRA_FAILURE', 153 Canceled = 'CANCELED', 154 } 155 156 export enum BuildStatusMask { 157 EndedMask = 'ENDED_MASK', 158 } 159 160 export interface TestPresentationConfig { 161 /** 162 * A list of keys that will be rendered as columns in the test results tab. 163 * status is always the first column and name is always the last column (you 164 * don't need to specify them). 165 * 166 * A key must be one of the following: 167 * 1. 'v.{variant_key}': variant.def[variant_key] of the test variant (e.g. 168 * v.gpu). 169 */ 170 column_keys?: string[]; 171 /** 172 * A list of keys that will be used for grouping test variants in the test 173 * results tab. 174 * 175 * A key must be one of the following: 176 * 1. 'status': status of the test variant. 177 * 2. 'name': test_metadata.name of the test variant. 178 * 3. 'v.{variant_key}': variant.def[variant_key] of the test variant (e.g. 179 * v.gpu). 180 * 181 * Caveat: test variants with only expected results are not affected by this 182 * setting and are always in their own group. 183 */ 184 grouping_keys?: string[]; 185 } 186 187 export interface BuildInput { 188 readonly properties?: { 189 [TEST_PRESENTATION_KEY]?: TestPresentationConfig; 190 [key: string]: unknown; 191 }; 192 readonly gitilesCommit?: GitilesCommit; 193 readonly gerritChanges?: GerritChange[]; 194 readonly experiments?: string[]; 195 } 196 197 export interface BuildOutput { 198 readonly properties?: { 199 [TEST_PRESENTATION_KEY]?: TestPresentationConfig; 200 [BLAMELIST_PIN_KEY]?: GitilesCommit[]; 201 [key: string]: unknown; 202 }; 203 readonly gitilesCommit?: GitilesCommit; 204 readonly logs: Log[]; 205 } 206 207 export interface Log { 208 readonly name: string; 209 readonly viewUrl: string; 210 readonly url: string; 211 } 212 213 export interface Step { 214 readonly name: string; 215 readonly startTime?: string; 216 readonly endTime?: string; 217 readonly status: BuildbucketStatus; 218 readonly logs?: Log[]; 219 readonly summaryMarkdown?: string; 220 readonly tags?: readonly StringPair[]; 221 } 222 223 export interface BuildInfra { 224 readonly swarming: BuildInfraSwarming; 225 readonly resultdb?: BuildInfraResultdb; 226 readonly buildbucket?: BuildInfraBuildbucket; 227 readonly backend?: BuildInfraBackend; 228 } 229 230 export interface BuildInfraBuildbucket { 231 readonly serviceConfigRevision: string; 232 readonly requestedProperties: { [key: string]: unknown }; 233 readonly requestedDimensions: RequestedDimension[]; 234 readonly hostname: string; 235 readonly agent?: BuildAgent; 236 } 237 238 export interface BuildAgent { 239 readonly input: BuildAgentInput; 240 readonly output?: BuildAgentOutput; 241 } 242 243 export interface BuildAgentInput { 244 readonly data: { [key: string]: BuildAgentInputDataRef }; 245 } 246 247 export interface BuildAgentInputDataRef { 248 readonly cipd: Cipd; 249 readonly onPath: string[]; 250 } 251 252 export interface BuildAgentOutput { 253 readonly resolvedData: { [key: string]: BuildAgentResolvedDataRef }; 254 readonly status: BuildbucketStatus; 255 readonly summaryHtml: string; 256 readonly agentPlatform: string; 257 readonly totalDuration: string; 258 } 259 260 export interface BuildAgentResolvedDataRef { 261 readonly cipd: Cipd; 262 } 263 264 export interface Cipd { 265 readonly specs: PkgSpec[]; 266 } 267 268 export interface PkgSpec { 269 readonly package: string; 270 readonly version: string; 271 } 272 273 export interface RequestedDimension { 274 readonly key: string; 275 readonly value: string; 276 readonly expiration: string; 277 } 278 279 export interface BuildInfraSwarming { 280 readonly hostname: string; 281 readonly taskId?: string; 282 readonly parentRunId?: string; 283 readonly taskServiceAccount: string; 284 readonly priority: number; 285 readonly taskDimensions: readonly RequestedDimension[]; 286 readonly botDimensions?: StringPair[]; 287 readonly caches: readonly BuildInfraSwarmingCacheEntry[]; 288 } 289 290 export interface BuildInfraSwarmingCacheEntry { 291 readonly name: string; 292 readonly path: string; 293 readonly waitForWarmCache: string; 294 readonly envVar: string; 295 } 296 297 export interface BuildInfraLogDog { 298 readonly hostname: string; 299 readonly project: string; 300 readonly prefix: string; 301 } 302 303 export interface BuildInfraRecipe { 304 readonly cipdPackage: string; 305 readonly name: string; 306 } 307 308 export interface BuildInfraResultdb { 309 readonly hostname: string; 310 readonly invocation?: string; 311 } 312 313 export interface BuildInfraBackend { 314 readonly config: { [key: string]: unknown }; 315 readonly task: Task; 316 readonly hostname: string; 317 } 318 319 export interface Task { 320 readonly id: TaskID; 321 readonly link?: string; 322 readonly status: BuildbucketStatus; 323 readonly statusDetails: StatusDetails; 324 readonly summaryHtml?: string; 325 readonly details: { [key: string]: unknown }; 326 readonly updateId: string; 327 } 328 329 export interface TaskID { 330 readonly target: string; 331 readonly id: string; 332 } 333 334 export interface StatusDetails { 335 readonly resourceExhaustion?: Record<string, never>; 336 readonly timeout?: Record<string, never>; 337 } 338 339 export interface Executable { 340 readonly cipdPackage?: string; 341 readonly cipdVersion?: string; 342 readonly cmd?: readonly string[]; 343 } 344 345 export interface CancelBuildRequest { 346 id: string; 347 summaryMarkdown: string; 348 fields?: string; 349 } 350 351 export interface ScheduleBuildRequest { 352 requestId?: string; 353 templateBuildId?: string; 354 builder?: BuilderID; 355 experiments?: { [key: string]: boolean }; 356 properties?: object; 357 gitilesCommit?: GitilesCommit; 358 gerritChanges?: GerritChange[]; 359 tags?: StringPair[]; 360 dimensions?: RequestedDimension[]; 361 priority?: string; 362 notify?: Notification; 363 fields?: string; 364 critical?: Trinary; 365 exe?: Executable; 366 swarming?: { 367 parentRunId: string; 368 }; 369 } 370 371 export class BuildsService { 372 static readonly SERVICE = 'buildbucket.v2.Builds'; 373 private readonly cachedCallFn: ( 374 opt: CacheOption, 375 method: string, 376 message: object, 377 ) => Promise<unknown>; 378 379 constructor(client: PrpcClientExt) { 380 this.cachedCallFn = cached( 381 (method: string, message: object) => 382 client.call(BuildsService.SERVICE, method, message), 383 { 384 key: (method, message) => `${method}-${stableStringify(message)}`, 385 }, 386 ); 387 } 388 389 async getBuild(req: GetBuildRequest, cacheOpt: CacheOption = {}) { 390 return (await this.cachedCallFn(cacheOpt, 'GetBuild', req)) as Build; 391 } 392 393 async searchBuilds(req: SearchBuildsRequest, cacheOpt: CacheOption = {}) { 394 return (await this.cachedCallFn( 395 cacheOpt, 396 'SearchBuilds', 397 req, 398 )) as SearchBuildsResponse; 399 } 400 401 async cancelBuild(req: CancelBuildRequest) { 402 return (await this.cachedCallFn( 403 { acceptCache: false, skipUpdate: true }, 404 'CancelBuild', 405 req, 406 )) as Build; 407 } 408 409 async scheduleBuild(req: ScheduleBuildRequest) { 410 return (await this.cachedCallFn( 411 { acceptCache: false, skipUpdate: true }, 412 'ScheduleBuild', 413 req, 414 )) as Build; 415 } 416 } 417 418 export interface GetBuilderRequest { 419 readonly id: BuilderID; 420 readonly mask?: { type: BuilderMask }; 421 } 422 423 export interface BuilderConfig { 424 readonly swarmingHost?: string; 425 readonly dimensions?: readonly string[]; 426 readonly descriptionHtml?: string; 427 } 428 429 export interface BuilderMetadata { 430 readonly health?: HealthStatus; 431 } 432 433 export interface HealthStatus { 434 readonly healthScore?: string; 435 readonly healthMetrics?: { readonly [key: string]: number }; 436 readonly description?: string; 437 readonly docLinks?: { readonly [domain: string]: string }; 438 readonly dataLinks?: { readonly [domain: string]: string }; 439 readonly reporter?: string; 440 readonly reportedTime?: string; 441 } 442 443 export interface BuilderItem { 444 readonly id: BuilderID; 445 readonly config: BuilderConfig; 446 readonly metadata?: BuilderMetadata; 447 } 448 449 export interface ListBuildersRequest { 450 readonly project?: string; 451 readonly bucket?: string; 452 readonly pageSize?: number; 453 readonly pageToken?: string; 454 } 455 456 export interface ListBuildersResponse { 457 readonly builders?: readonly BuilderItem[]; 458 readonly nextPageToken?: string; 459 } 460 461 export class BuildersService { 462 static readonly SERVICE = 'buildbucket.v2.Builders'; 463 464 private readonly cachedCallFn: ( 465 opt: CacheOption, 466 method: string, 467 message: object, 468 ) => Promise<unknown>; 469 470 constructor(client: PrpcClientExt) { 471 this.cachedCallFn = cached( 472 (method: string, message: object) => 473 client.call(BuildersService.SERVICE, method, message), 474 { key: (method, message) => `${method}-${stableStringify(message)}` }, 475 ); 476 } 477 478 async getBuilder(req: GetBuilderRequest, cacheOpt: CacheOption = {}) { 479 return (await this.cachedCallFn( 480 cacheOpt, 481 'GetBuilder', 482 req, 483 )) as BuilderItem; 484 } 485 486 async listBuilders(req: ListBuildersRequest, cacheOpt: CacheOption = {}) { 487 return (await this.cachedCallFn( 488 cacheOpt, 489 'ListBuilders', 490 req, 491 )) as ListBuildersResponse; 492 } 493 } 494 495 export function getAssociatedGitilesCommit(build: Build): GitilesCommit | null { 496 return build.output?.gitilesCommit || build.input?.gitilesCommit || null; 497 }