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  }