| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import { |
| ChangeInfo, |
| RevisionInfo, |
| } from '@gerritcodereview/typescript-api/rest-api'; |
| import {Builder, BuildStatus} from './buildbucket-client'; |
| import {Build, Config} from './checks-fetcher'; |
| |
| /** |
| * Compares build IDs by creation time. |
| * |
| * Build IDs are monotonically decreasing. |
| * |
| * A Buildbucket build ID is a positive int64. This is too large for |
| * JavaScript numbers; we cannot just parse them as integers, so we |
| * keep them as strings. |
| * |
| * @return Negative if `a` should go before `b`, |
| * 0 if they're equal, and positive if `a` should go after `b`. |
| */ |
| export function compareBuildIds(a: string, b: string): number { |
| const d = b.length - a.length; |
| if (d != 0) { |
| return d; |
| } |
| if (a > b) { |
| return -1; |
| } |
| if (a < b) { |
| return 1; |
| } |
| return 0; |
| } |
| |
| /** |
| * Constructs a string used to uniquely identify a BB operation. |
| * |
| * @return A client operation ID string. |
| */ |
| export function getNewOperationId(): string { |
| return `opid:${Date.now()}:${Math.random().toString(36)}`; |
| } |
| |
| /* |
| * Converts string representation of legacy bucket definition (i.e. |
| * luci.project.bucket) to object with project and bucket properties. |
| */ |
| export function convertLegacyBucket(bucket: string): Builder { |
| const luciPrefixLength = 'luci.'.length; |
| const sepIndex = bucket.indexOf('.', luciPrefixLength); |
| return { |
| project: bucket.slice(luciPrefixLength, sepIndex), |
| bucket: bucket.slice(sepIndex + 1), |
| }; |
| } |
| |
| /* |
| * A helper function for checking build tags. |
| */ |
| function checkTag(build: Build, key: string, check: (arg0: string) => boolean) { |
| return (build.tags || []).some(t => t.key === key && check(t.value)); |
| } |
| |
| /* |
| * Returns true if a build is either CQ experimental or LUCI experimental. |
| */ |
| export function isExperimental(build: Build): boolean { |
| return ( |
| (build.input?.experiments || []).includes('luci.non_production') || |
| checkTag(build, 'cq_experimental', v => v === 'true') |
| ); |
| } |
| |
| /* |
| * Returns true if builders have the same project and bucket. |
| */ |
| export function identicalBucket(builder1: Builder, builder2: Builder): boolean { |
| return ( |
| builder1.project === builder2.project && builder1.bucket === builder2.bucket |
| ); |
| } |
| |
| /** |
| * Returns whether the 'Choose Tryjobs' button should appear. |
| * |
| * @param config Buildbucket plugin config. |
| * @param loggedIn Whether the user is logged in. |
| * @param change Change object. |
| */ |
| export function showChooseTryjobs( |
| config: Config | null, |
| loggedIn: boolean, |
| change: ChangeInfo |
| ): boolean { |
| return !!(config?.buckets?.length && loggedIn && change.status === 'NEW'); |
| } |
| |
| /** |
| * Returns whether the 'Retry Failed', 'Run', or 'Cancel' build buttons should |
| * show. |
| * |
| * @param config Buildbucket plugin config. |
| * @param loggedIn Whether the user is logged in. |
| * @param change Change object. |
| */ |
| export function showRetryFailed( |
| config: Config | null, |
| loggedIn: boolean, |
| change: ChangeInfo |
| ): boolean { |
| return !!( |
| config && |
| !config.hideRetryButton && |
| loggedIn && |
| change.status === 'NEW' |
| ); |
| } |
| |
| /** |
| * Returns whether the given build should skip retry. |
| * |
| * @param build Build object. |
| * @return boolean whether the given build should be skipped when the |
| * "retry failed" button is used. |
| */ |
| export function shouldSkipRetry(build: Build): boolean { |
| return checkTag(build, 'skip-retry-in-gerrit', v => v !== 'false'); |
| } |
| |
| /** |
| * Returns whether the given build has a tag for hiding in gerrit. |
| * |
| * @param build Build object. |
| * @return boolean whether the given build has a tag for hiding in gerrit. |
| */ |
| export function hasHideInGerritTag(build: Build): boolean { |
| return checkTag(build, 'hide-in-gerrit', v => v !== 'false'); |
| } |
| |
| /** |
| * Returns whether the given build has a tag for hiding ResultDB test results in |
| * gerrit. |
| * |
| * @param build Build object. |
| * @return boolean whether the given build has a tag for hiding ResultDB test |
| * results in gerrit. |
| */ |
| export function hasHideTestResultsInGerritTag(build: Build): boolean { |
| return checkTag(build, 'hide-test-results-in-gerrit', v => v !== 'false'); |
| } |
| |
| /** |
| * Divides the given builds into regular and experimental groups. |
| * |
| * @param allBuilds All builds in a flat Array. |
| * @return Two Arrays; the first with regular builds |
| * and the second with experimental builds. |
| */ |
| export function splitExperimentalBuilds(allBuilds: Build[]): Build[][] { |
| const builds: Build[] = []; |
| const experimentalBuilds: Build[] = []; |
| allBuilds.forEach(b => { |
| if (isExperimental(b)) { |
| experimentalBuilds.push(b); |
| } else { |
| builds.push(b); |
| } |
| }); |
| return [builds, experimentalBuilds]; |
| } |
| |
| /** |
| * Returns the buildIds for failed builds that should be retried. |
| * |
| * We want to retry builders when: |
| * - There are no running builds. |
| * - The latest completed build is failed (FAILURE or INFRA_FAILURE). |
| * - The builder is non-experimental. |
| * |
| * @param allBuilds Build objects from Buildbucket. These |
| * have a builder property which itself is an object with project, |
| * bucket, and builder string properties (where bucket is short name, |
| * e.g. try). |
| * @return An array of buildIds of failed builds where a retry has not |
| * already started. |
| */ |
| export function getRetryTemplateBuildIds(allBuilds: Build[]): string[] { |
| // Only consider non-experimental builds. |
| const [builds] = splitExperimentalBuilds(allBuilds); |
| const buildIds: Map<string, string> = new Map(); |
| const seen = new Set(); |
| // Sort builds from latest to oldest. |
| builds.sort((a, b) => -compareBuildIds(a.id, b.id)); |
| builds.forEach(b => { |
| const builder = b.builder; |
| const key = `${builder.project}/${builder.bucket}/${builder.builder}`; |
| // We only care if the latest build for a builder has failed. |
| if ( |
| !seen.has(key) && |
| !shouldSkipRetry(b) && |
| [BuildStatus.FAILURE, BuildStatus.INFRA_FAILURE].includes(b.status) |
| ) { |
| buildIds.set(key, b.id); |
| } |
| // If a build for this builder has already started, we don't need to |
| // retry again. |
| if (buildIds.has(key) && b.status == BuildStatus.STARTED) { |
| buildIds.delete(key); |
| } |
| seen.add(key); |
| }); |
| return Array.from(buildIds.values()); |
| } |
| |
| /** |
| * Returns the revisions in a change from latest to earliest. |
| * |
| * @param change A Gerrit ChangeInfo object. |
| * @return Gerrit RevisionInfo objects, which all have |
| * a _number property; ordered by _number from highest to lowest. |
| */ |
| export function reversedRevisions(change: ChangeInfo): RevisionInfo[] { |
| const revisions: RevisionInfo[] = []; |
| for (const revision in change.revisions) { |
| const rev = change.revisions[revision]; |
| if (rev && typeof rev._number === 'number') { |
| revisions.push(rev); |
| } |
| } |
| revisions.sort( |
| (a, b) => |
| // Reverse sort. |
| (b._number as number) - (a._number as number) |
| ); |
| return revisions; |
| } |
| |
| /** |
| * Checks whether the currently-displayed patchset is the latest patchset. |
| * |
| * @param change The Gerrit change. |
| * @param patchNum The current patchset number. |
| * @returns true if the current patchset is the latest. |
| */ |
| export function isLatestPatchset( |
| change: ChangeInfo, |
| patchNum: number |
| ): boolean { |
| const revs = reversedRevisions(change); |
| return revs.length > 0 && revs[0]._number === patchNum; |
| } |
| |
| /** |
| * Groups the patchsets in the change into sets of equivalent patchsets. |
| * |
| * @param change A Gerrit ChangeInfo object. |
| * @return Groups of equivalent patchsets, from latest to |
| * earliest. |
| */ |
| export function splitPatchsetGroups(change: ChangeInfo): Set<number>[] { |
| const trivialKinds = new Set([ |
| 'TRIVIAL_REBASE', |
| 'NO_CHANGE', |
| 'NO_CODE_CHANGE', |
| ]); |
| const revisions = reversedRevisions(change); |
| const equivalentSets: Set<number>[] = []; |
| let currentSet: Set<number> = new Set(); |
| for (let i = 0; i < revisions.length; i++) { |
| currentSet.add(revisions[i]._number as number); |
| if (!trivialKinds.has(revisions[i].kind)) { |
| // If this revision was a non-trivial change, it is not part |
| // of the same equivalent patchset set as those after it. |
| equivalentSets.push(currentSet); |
| currentSet = new Set(); |
| } |
| } |
| console.assert( |
| currentSet.size === 0, |
| 'the first patchset is expected to be non-trivial' |
| ); |
| return equivalentSets; |
| } |
| |
| /** |
| * Returns a Date object from a Timestamp string defined in: |
| * https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto |
| * |
| * @param timestamp A timestamp string with the above format. |
| */ |
| export function getDateFromTimestamp( |
| timestamp: string | undefined |
| ): Date | undefined { |
| if (!timestamp) { |
| return undefined; |
| } |
| const m = |
| /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?Z$/.exec( |
| String(timestamp) |
| ); |
| if (!m) { |
| return undefined; |
| } |
| |
| // m[2] matches the month with January being 01. The Date constructor takes a |
| // 0-indexed month. |
| // |
| // m[7] matches the fractional seconds of timestamp and takes the format |
| // ".[0-9]{1-9}". This gets an integer number of milliseconds given m[7]. |
| return new Date( |
| Date.UTC( |
| Number(m[1]), |
| Number(m[2]) - 1, |
| Number(m[3]), |
| Number(m[4]), |
| Number(m[5]), |
| Number(m[6]), |
| Math.round(Number(m[7] || '0') * 1000) |
| ) |
| ); |
| } |
| |
| /** |
| * Returns the full resource name for a given Buildbucket host and build ID. |
| */ |
| export function createBuildResourceName(host: string, buildId: string): string { |
| return `//${host}/build/${buildId}`; |
| } |
| |
| /** |
| * Returns a url to the build page for a given Buildbucket host and build ID. |
| */ |
| export function createBuildUrl(host: string, buildId: string): string { |
| return encodeURI(`https:${createBuildResourceName(host, buildId)}`); |
| } |
| |
| /** |
| * Returns a link to the builder page for the given build. |
| */ |
| export function createBuilderLink(build: Build): string { |
| const builder = build.builder; |
| return encodeURI( |
| `https://ci.chromium.org/p/${builder.project}/builders/` + |
| `${builder.bucket}/${builder.builder}` |
| ); |
| } |