UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

215 lines (199 loc) 7.49 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 { Configuration, configurationValue, GitProject, HandlerContext, logger, Project, RemoteRepoRef, } from "@atomist/automation-client"; import { execPromise, PreferenceStore, PushImpactListenerInvocation, } from "@atomist/sdm"; import { toArray } from "@atomist/sdm-core/lib/util/misc/array"; import { Aspect, FP, VirtualProjectFinder, } from "@atomist/sdm-pack-fingerprint"; import { toName } from "@atomist/sdm-pack-fingerprint/lib/adhoc/preferences"; import { Analyzed } from "../../../aspect/AspectRegistry"; import { time } from "../../../util/showTiming"; import { AspectBeingTracked, RepoBeingTracked, } from "../../tracking/analysisTracker"; import { Analyzer, TimeRecorder, } from "./Spider"; /** * Analyzer implementation that captures timings that are useful during * development, but don't need to be captured during regular execution. */ export class SpiderAnalyzer implements Analyzer { public readonly timings: TimeRecorder = {}; public async analyze(p: Project, repoTracking: RepoBeingTracked): Promise<Analyzed> { const fingerprints: FP[] = []; if (this.virtualProjectFinder) { // Seed the virtual project finder if we have one await this.virtualProjectFinder.findVirtualProjectInfo(p); } const pili = await fakePushImpactListenerInvocation(p); await runExtracts(p, pili, this.aspects, fingerprints, this.timings, repoTracking); await runConsolidates(p, pili, this.aspects.filter(aspect => !!aspect.consolidate), fingerprints, repoTracking); return { id: p.id as RemoteRepoRef, fingerprints, }; } constructor(private readonly aspects: Aspect[], private readonly virtualProjectFinder?: VirtualProjectFinder) { } } async function runExtracts(p: Project, pili: PushImpactListenerInvocation, aspects: Aspect[], fingerprints: FP[], timings: TimeRecorder, repoTracking: RepoBeingTracked): Promise<void> { await Promise.all(aspects .map(aspect => safeTimedExtract(aspect, p, pili, timings, repoTracking.plan(aspect, "extract")) .then(fps => fingerprints.push(...fps), ))); } /** * This will run the consolidation aspects sequentially in order, passing in the result of previous consolidated fingerprints into * later aspects that consolidate. */ async function runConsolidates(p: Project, pili: PushImpactListenerInvocation, aspects: Aspect[], fingerprints: FP[], repoTracking: RepoBeingTracked): Promise<void> { for (const aspect of aspects) { const consolidatedFingerprints = await safeConsolidate(aspect, fingerprints, p, pili, repoTracking.plan(aspect, "consolidate")); fingerprints.push(...consolidatedFingerprints); } } async function safeTimedExtract(aspect: Aspect, p: Project, pili: PushImpactListenerInvocation, timeRecorder: TimeRecorder, tracking: AspectBeingTracked): Promise<FP[]> { try { const timed = await time(async () => { const fps = toArray(await aspect.extract(p, pili)) || []; fps.forEach(fp => { if (!fp.displayName && aspect.toDisplayableFingerprintName) { fp.displayName = aspect.toDisplayableFingerprintName(toName(fp.type, fp.name)); } if (!fp.displayValue && aspect.toDisplayableFingerprint) { fp.displayValue = aspect.toDisplayableFingerprint(fp); } return fp; }); return fps; }); addTiming(aspect.name, timed.millis, timeRecorder); const result = !!timed.result ? toArray(timed.result) : []; tracking.completed(result.length); return result; } catch (err) { tracking.failed(err); logger.error("Please check your configuration of aspect %s.\n%s", aspect.name, err); return []; } } function addTiming(type: string, millis: number, timeRecorder: TimeRecorder): void { let found = timeRecorder[type]; if (!found) { found = { extractions: 0, totalMillis: 0, }; timeRecorder[type] = found; } found.extractions++; found.totalMillis += millis; } async function safeConsolidate(aspect: Aspect, existingFingerprints: FP[], p: Project, pili: PushImpactListenerInvocation, tracking: AspectBeingTracked): Promise<FP[]> { try { const extracted = await aspect.consolidate(existingFingerprints, p, pili); const result = !!extracted ? toArray(extracted) : []; tracking.completed(result.length); return result; } catch (err) { tracking.failed(err); logger.error("Please check your configuration of aspect %s.\n%s", aspect.name, err); return []; } } async function fetchChangedFiles(project: GitProject): Promise<string[]> { try { const output = await execPromise("git", ["show", `--pretty=format:""`, "--name-only"], { cwd: project.baseDir }); return output.stdout.trim().split("\n"); } catch (err) { logger.error("Failure getting changed files: %s", err.message); return []; } } /** * Make a fake push for the last commit to this project */ async function fakePushImpactListenerInvocation(p: Project): Promise<PushImpactListenerInvocation> { const project = p as GitProject; const changedFiles = await fetchChangedFiles(project); return { id: p.id as any, get context(): HandlerContext { logger.warn("Returning undefined context"); return undefined; }, commit: { sha: p.id.sha, }, project, push: { repo: undefined, branch: "master", }, addressChannels: async () => { logger.warn("Cannot say anything in local mode"); }, get filesChanged(): string[] { return changedFiles; }, credentials: { token: process.env.GITHUB_TOKEN }, impactedSubProject: p, get preferences(): PreferenceStore { logger.warn("Returning undefined preferences store"); return undefined; }, configuration: configurationValue<Configuration>("", {}), }; }