-
Notifications
You must be signed in to change notification settings - Fork 532
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor bundle buddy to be more re-usable in fluidframework (#3207)
Refactor our copy of bundle buddy to be reusable in fluidframework. This change is a minimal set of changes needed for us to write a wrapper around it to achieve our goals around package size reporting (in a follow up review, added to a different package). This package will be further refactored after so that bohemia can consume it in a similar way.
- Loading branch information
Showing
16 changed files
with
740 additions
and
385 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
/*! | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. | ||
*/ | ||
|
||
import { WebApi } from 'azure-devops-node-api'; | ||
import JSZip from 'jszip'; | ||
import { join } from 'path'; | ||
import { getBaselineCommit, getBuilds, getPriorCommit } from '../utilities'; | ||
import { getAzureDevopsApi } from './getAzureDevopsApi'; | ||
import { BuildStatus, BuildResult } from 'azure-devops-node-api/interfaces/BuildInterfaces'; | ||
import { IADOConstants } from './Constants'; | ||
import { getZipObjectFromArtifact, getBundlePathsFromZipObject, getStatsFileFromZip } from './AdoArtifactFileProvider'; | ||
import { | ||
getBundlePathsFromFileSystem, | ||
getStatsFileFromFileSystem, | ||
getBundleBuddyConfigFromFileSystem | ||
} from './FileSystemBundleFileProvider'; | ||
import { getBuildTagForCommit } from './getBuildTagForCommit'; | ||
import { getCommentForBundleDiff, getSimpleComment } from './getCommentForBundleDiff'; | ||
import { DefaultStatsProcessors } from './DefaultStatsProcessors'; | ||
import { compareBundles } from '../compareBundles'; | ||
import { getBundleSummaries } from './getBundleSummaries'; | ||
import { getBundleBuddyConfigMap } from './getBundleBuddyConfigMap'; | ||
|
||
export class ADOSizeComparator { | ||
constructor( | ||
/** | ||
* ADO constants identifying where to fetch baseline bundle info | ||
*/ | ||
private readonly adoConstants: IADOConstants, | ||
/** | ||
* The ADO connection to use to fetch baseline bundle info | ||
*/ | ||
private readonly adoConnection: WebApi, | ||
/** | ||
* Path to existing local bundle size reports | ||
*/ | ||
private readonly localReportPath: string, | ||
/** | ||
* Optional current PR build id to use, such as to tag for | ||
* later update when the baseline build has not completed | ||
*/ | ||
private readonly adoBuildId: number | undefined, | ||
/** | ||
* Option to do fallback on commits when either there is no associated CI build or | ||
* it does not have the needed artifacts. Fallback is not attempted for other | ||
* issues, such as for a failed (but still present) CI build. This generator is | ||
* only used for fallback (it should not provide the first commit to check) | ||
*/ | ||
private readonly getFallbackCommit: ((startingCommit: string) => Generator<string>) | undefined = undefined | ||
) {} | ||
|
||
/** | ||
* Naive fallback generator provided for convenience. It yields the commit directly | ||
* prior to the previous commit. | ||
*/ | ||
public static * naiveFallbackCommitGenerator(startingCommit: string): Generator<string> { | ||
let currentCommit = startingCommit; | ||
for (let i = 0; i < 5; i++) { | ||
currentCommit = getPriorCommit(currentCommit); | ||
yield currentCommit; | ||
} | ||
} | ||
|
||
/** | ||
* Create a size comparison message that can be posted to a PR | ||
* @param tagWaiting - If the build should be tagged to be updated when the baseline | ||
* build completes (if it wasn't already complete when the comparison runs) | ||
* @returns The size comparison message | ||
*/ | ||
public async createSizeComparisonMessage(tagWaiting: boolean): Promise<string> { | ||
let baselineCommit: string | undefined = getBaselineCommit(); | ||
console.log(`The baseline commit for this PR is ${baselineCommit}`); | ||
|
||
// Some circumstances may want us to try a fallback, such as when a commit does | ||
// not trigger any CI loops. If a fallback generator is provided, use that. | ||
let baselineZip; | ||
const fallbackGen = this.getFallbackCommit?.(baselineCommit!); | ||
const recentBuilds = await getBuilds(this.adoConnection, { | ||
project: this.adoConstants.projectName, | ||
definitions: [this.adoConstants.ciBuildDefinitionId], | ||
maxBuildsPerDefinition: this.adoConstants.buildsToSearch ?? 20 | ||
}); | ||
while (baselineCommit !== undefined) { | ||
let baselineBuild = recentBuilds.find((build) => build.sourceVersion === baselineCommit); | ||
|
||
if (baselineBuild === undefined) { | ||
baselineCommit = fallbackGen?.next().value; | ||
console.log(`Trying backup baseline commit ${baselineCommit}`); | ||
continue; | ||
} | ||
|
||
// Baseline build does not have id | ||
if (baselineBuild.id === undefined) { | ||
const message = `Baseline build does not have a build id`; | ||
console.log(message); | ||
return message; | ||
} | ||
|
||
// Baseline build is pending | ||
if (baselineBuild.status !== BuildStatus.Completed) { | ||
const message = getSimpleComment('Baseline build for this PR has not yet completed.', baselineCommit); | ||
console.log(message); | ||
|
||
if (tagWaiting) { | ||
this.tagBuildAsWaiting(baselineCommit); | ||
} | ||
|
||
return message; | ||
} | ||
|
||
// Baseline build failed | ||
if (baselineBuild.result !== BuildResult.Succeeded) { | ||
const message = getSimpleComment( | ||
'Baseline CI build failed, cannot generate bundle analysis at this time', | ||
baselineCommit | ||
); | ||
console.log(message); | ||
return message; | ||
} | ||
|
||
// Baseline build succeeded | ||
console.log(`Found baseline build with id: ${baselineBuild.id}`); | ||
baselineZip = await getZipObjectFromArtifact( | ||
this.adoConnection, | ||
this.adoConstants.projectName, | ||
baselineBuild.id, | ||
this.adoConstants.bundleAnalysisArtifactName).catch(() => { | ||
return undefined; | ||
}); | ||
|
||
// Successful baseline build does not have the needed build artifacts | ||
if (baselineZip === undefined) { | ||
baselineCommit = this.getFallbackCommit?.(baselineCommit).next().value; | ||
console.log(`Trying backup baseline commit ${baselineCommit}`); | ||
continue; | ||
} | ||
|
||
// Found usable baseline zip | ||
break; | ||
} | ||
|
||
// Unable to find a usable baseline | ||
if (baselineCommit === undefined || baselineZip === undefined) { | ||
const message = `Could not find a usable baseline build with search starting at CI ${getBaselineCommit()}`; | ||
console.log(message); | ||
return message; | ||
} | ||
|
||
const message = await this.createMessageFromZip(baselineCommit, baselineZip); | ||
console.log(message); | ||
return message; | ||
} | ||
|
||
private async tagBuildAsWaiting(baselineCommit: string): Promise<void> { | ||
if (!this.adoBuildId) { | ||
console.log( | ||
'No ADO build ID was provided, we will not tag this build for follow up when the baseline build completes' | ||
); | ||
} else { | ||
// Tag the current build as waiting for the results of the master CI | ||
const buildApi = await this.adoConnection.getBuildApi(); | ||
await buildApi.addBuildTag(this.adoConstants.projectName, this.adoBuildId, getBuildTagForCommit(baselineCommit)); | ||
} | ||
} | ||
|
||
private async createMessageFromZip(baselineCommit: string, baselineZip: JSZip): Promise<string> { | ||
const baselineZipBundlePaths = getBundlePathsFromZipObject(baselineZip); | ||
|
||
const prBundleFileSystemPaths = await getBundlePathsFromFileSystem(this.localReportPath); | ||
|
||
const configFileMap = await getBundleBuddyConfigMap({ | ||
bundleFileData: prBundleFileSystemPaths, | ||
getBundleBuddyConfig: (relativePath) => | ||
getBundleBuddyConfigFromFileSystem(join(this.localReportPath, relativePath)) | ||
}); | ||
|
||
const baselineSummaries = await getBundleSummaries({ | ||
bundlePaths: baselineZipBundlePaths, | ||
getStatsFile: (relativePath) => getStatsFileFromZip(baselineZip, relativePath), | ||
getBundleBuddyConfigFile: (bundleName) => configFileMap.get(bundleName), | ||
statsProcessors: DefaultStatsProcessors | ||
}); | ||
|
||
const prSummaries = await getBundleSummaries({ | ||
bundlePaths: prBundleFileSystemPaths, | ||
getStatsFile: (relativePath) => getStatsFileFromFileSystem(join(this.localReportPath, relativePath)), | ||
getBundleBuddyConfigFile: (bundleName) => configFileMap.get(bundleName), | ||
statsProcessors: DefaultStatsProcessors | ||
}); | ||
|
||
const bundleComparisons = compareBundles(baselineSummaries, prSummaries); | ||
|
||
console.log(JSON.stringify(bundleComparisons)); | ||
|
||
const message = getCommentForBundleDiff(bundleComparisons, baselineCommit); | ||
return message; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.