UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

249 lines (221 loc) 7.96 kB
/* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { GitProject, logger, Project, RepoId, RepoRef, } from "@atomist/automation-client"; import { Analyzed } from "../../../aspect/AspectRegistry"; import { AnalysisTracking, RepoBeingTracked, } from "../../tracking/analysisTracker"; import { ProjectAnalysisResultStore, } from "../persist/ProjectAnalysisResultStore"; import { computeAnalytics } from "./analytics"; import { Analyzer, ProjectAnalysisResultFilter, SpiderResult, } from "./Spider"; /** * A reasonable number of repositories to analyze at a time. * The bucketing mechanism will wait for this many to complete, then * start another batch. If this is too big, the web interface can't respond. */ export const DefaultPoolSize = 6; interface TrackedRepo<FoundRepo> { foundRepo: FoundRepo; tracking: RepoBeingTracked; repoRef?: RepoRef; } /** * This class knows how to execute an analysis run, * given some functions that are specific to the source * of the repositories. */ export class AnalysisRun<FoundRepo> { constructor( private readonly world: { howToFindRepos: () => AsyncIterable<FoundRepo>, determineRepoRef: (f: FoundRepo) => Promise<RepoRef>, describeFoundRepo: (f: FoundRepo) => RepoDescription, howToClone: (rr: RepoRef, fr: FoundRepo) => Promise<GitProject>, analyzer: Analyzer; persister: ProjectAnalysisResultStore, analysisTracking: AnalysisTracking, keepExistingPersisted: ProjectAnalysisResultFilter, projectFilter?: (p: Project) => Promise<boolean>; }, private readonly params: { workspaceId: string; description: string; maxRepos?: number; poolSize?: number; }) { if (!this.params.maxRepos) { this.params.maxRepos = 1000; } if (!this.params.poolSize) { this.params.poolSize = DefaultPoolSize; } } public async run(): Promise<SpiderResult> { const analysisBeingTracked = this.world.analysisTracking.startAnalysis({ description: this.params.description, }); try { const plannedRepos = await takeFromIterator(this.params.maxRepos, this.world.howToFindRepos()); const trackedRepos: Array<TrackedRepo<FoundRepo>> = plannedRepos.map(pr => ({ tracking: analysisBeingTracked.plan(this.world.describeFoundRepo(pr)), foundRepo: pr, })); // run poolSize at the same time const chewThroughThese = trackedRepos.slice(); while (chewThroughThese.length > 0) { const promises = chewThroughThese.splice(0, this.params.poolSize) .map(trackedRepo => analyzeOneRepo(this.world, { ...trackedRepo, workspaceId: this.params.workspaceId })); await Promise.all(promises); } logger.debug("Computing analytics over all fingerprints..."); // Question for Rod: should this run intermittently or only at the end? // Answer from Rod: intermitently. await computeAnalytics(this.world.persister, this.params.workspaceId); const finalResult = trackedRepos.map(tr => tr.tracking.spiderResult()).reduce(combineSpiderResults, emptySpiderResult); analysisBeingTracked.stop(); return finalResult; } catch (error) { analysisBeingTracked.failed(error); return emptySpiderResult; } } } interface RepoDescription { description: string; url?: string; } async function analyzeOneRepo<FoundRepo>( world: { howToFindRepos: () => AsyncIterable<FoundRepo>, determineRepoRef: (f: FoundRepo) => Promise<RepoRef>, describeFoundRepo: (f: FoundRepo) => RepoDescription, howToClone: (rr: RepoRef, fr: FoundRepo) => Promise<GitProject>, analyzer: Analyzer; persister: ProjectAnalysisResultStore, keepExistingPersisted: ProjectAnalysisResultFilter, projectFilter?: (p: Project) => Promise<boolean>; }, params: { workspaceId: string, foundRepo: FoundRepo, tracking: RepoBeingTracked, }): Promise<void> { logger.info("Now analyzing: " + JSON.stringify(params.foundRepo)); const { tracking, workspaceId, foundRepo } = params; tracking.beganAnalysis(); const repoRef = await world.determineRepoRef(foundRepo); tracking.setRepoRef(repoRef); // we might choose to skip this one if (await existingRecordShouldBeKept(world, repoRef)) { // enhancement: record timestamp of kept record tracking.keptExisting(); return; } // clone let project: GitProject; try { project = await world.howToClone(repoRef, foundRepo); } catch (error) { tracking.failed({ whileTryingTo: "clone", error }); return; } // we might choose to skip this one (is this used anywhere?) if (world.projectFilter && !await world.projectFilter(project)) { tracking.skipped("projectFilter returned false"); return; } // analyze ! let analysis: Analyzed; try { analysis = await world.analyzer.analyze(project, tracking); } catch (error) { tracking.failed({ whileTryingTo: "analyze", error }); return; } // save :-) const persistResult = await world.persister.persist({ workspaceId, repoRef, analysis: { ...analysis, id: repoRef, // necessary? }, timestamp: new Date(), }); persistResult.failedFingerprints.forEach(f => { tracking.failFingerprint(f.failedFingerprint, f.error); }); if (persistResult.failed.length === 1) { tracking.failed(persistResult.failed[0]); } else if (persistResult.succeeded.length === 1) { tracking.persisted(persistResult.succeeded[0]); } else { throw new Error("Unexpected condition in persistResult: " + JSON.stringify(persistResult)); } } async function existingRecordShouldBeKept( opts: { persister: ProjectAnalysisResultStore, keepExistingPersisted: ProjectAnalysisResultFilter, }, repoId: RepoId): Promise<boolean> { const found = await opts.persister.loadByRepoRef(repoId, true); if (!found || !found.analysis) { return false; } return opts.keepExistingPersisted(found); } async function takeFromIterator<T>(max: number, iter: AsyncIterable<T>): Promise<T[]> { let i = 0; const result: T[] = []; for await (const t of iter) { if (++i > max) { return result; } result.push(t); } return result; } function combineSpiderResults(r1: SpiderResult, r2: SpiderResult): SpiderResult { return { repositoriesDetected: r1.repositoriesDetected + r2.repositoriesDetected, failed: [...r1.failed, ...r2.failed], keptExisting: [...r1.keptExisting, ...r2.keptExisting], persistedAnalyses: [...r1.persistedAnalyses, ...r2.persistedAnalyses], }; } const emptySpiderResult = { repositoriesDetected: 0, failed: [], keptExisting: [], persistedAnalyses: [], };