UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

344 lines (306 loc) 12.7 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 { BannerSection, Configuration, } from "@atomist/automation-client"; import { ExpressCustomizer } from "@atomist/automation-client/lib/configuration"; import { ExtensionPack, ExtensionPackMetadata, metadata, PushImpact, } from "@atomist/sdm"; import { DeliveryGoals, isInLocalMode, } from "@atomist/sdm-core"; import { toArray } from "@atomist/sdm-core/lib/util/misc/array"; import { Aspect, cachingVirtualProjectFinder, fileNamesVirtualProjectFinder, fingerprintSupport, PublishFingerprints, RebaseOptions, VirtualProjectFinder, } from "@atomist/sdm-pack-fingerprint"; import { AspectsFactory } from "@atomist/sdm-pack-fingerprint/lib/machine/fingerprintSupport"; import * as _ from "lodash"; import { sdmConfigClientFactory } from "../analysis/offline/persist/pgClientFactory"; import { ClientFactory } from "../analysis/offline/persist/pgUtils"; import { analyzeGitHubByQueryCommandRegistration, analyzeGitHubOrganizationCommandRegistration, analyzeLocalCommandRegistration, } from "../analysis/offline/spider/analyzeCommand"; import { AnalysisTracker, AnalysisTracking, } from "../analysis/tracking/analysisTracker"; import { RepositoryScorer, Tagger, TaggerDefinition, WorkspaceScorer, } from "../aspect/AspectRegistry"; import { isClassificationAspect, projectClassificationAspect, } from "../aspect/compose/classificationAspect"; import { DefaultAspectRegistry } from "../aspect/DefaultAspectRegistry"; import { isDeliveryAspect } from "../aspect/delivery/DeliveryAspect"; import { UndesirableUsageChecker } from "../aspect/ProblemStore"; import { AspectCompatibleScorer, emitScoringAspect, ScoredAspect, } from "../aspect/score/ScoredAspect"; import { calculateFingerprintTask } from "../job/fingerprintTask"; import { registerAspects } from "../job/registerAspect"; import { api } from "../routes/api"; import { addWebAppRoutes } from "../routes/web-app/webAppRoutes"; import { ScoreWeightings } from "../scorer/Score"; import { exposeFingerprintScore } from "../scorer/support/exposeFingerprintScore"; import { tagsFromClassificationFingerprints } from "../tagger/commonTaggers"; import { analysisResultStore, createAnalyzer, } from "./machine"; /** * Default VirtualProjectFinder, which recognizes Maven, npm, * and Gradle projects and Python projects using requirements.txt. */ export const DefaultVirtualProjectFinder: VirtualProjectFinder = // Consider directories containing any of these files to be virtual projects cachingVirtualProjectFinder( fileNamesVirtualProjectFinder( "package.json", "pom.xml", "build.gradle", "requirements.txt", )); export const DefaultScoreWeightings: ScoreWeightings = { // Weight this to penalize projects with few other scorers anchor: 3, }; /** * Options to configure the aspect extension pack */ export interface AspectSupportOptions { /** * Aspects that cause this SDM to calculate fingerprints from projects * and delivery events. */ aspects: Aspect | Aspect[]; /** * Dynamically add aspects based on current push */ aspectsFactory?: AspectsFactory; /** * If set, this enables multi-project support by helping aspects work * on virtual projects. For example, a VirtualProjectFinder may establish * that subdirectories with package.json or requirements.txt files are * subprojects, enabling aspects to work on their internal structure * without needing to drill into the entire repository themselves. */ virtualProjectFinder?: VirtualProjectFinder; /** * Registrations that can tag projects based on fingerprints. * Executed as fingerprints */ taggers?: Tagger | Tagger[]; /** * Registrations that can tag projects based on fingerprints. * Allows rapid in-memory use. */ inMemoryTaggers?: TaggerDefinition | TaggerDefinition[]; /** * Scoring fingerprints. Name to scorers */ scorers?: Record<string, AspectCompatibleScorer | AspectCompatibleScorer[]>; workspaceScorers?: WorkspaceScorer[]; /** * Scorers that are computed in memory. Allows for faster iteration on scoring logic. * May ultimately be promoted to scorers. */ inMemoryScorers?: RepositoryScorer | RepositoryScorer[]; /** * Optional weightings for different scorers. The key is scorer name. */ weightings?: ScoreWeightings; /** * Set this to flag undesirable fingerprints: For example, * a dependency you wish to eliminate from all projects, or * an internally inconsistent fingerprint state. */ undesirableUsageChecker?: UndesirableUsageChecker; /** * Custom fingerprint routing. Used in local mode. * Default behavior is to send fingerprints to Atomist. */ publishFingerprints?: PublishFingerprints; /** * Delivery goals to attach fingerprint behavior to, if provided. * Delivery goals must have well-known names */ goals?: Partial<Pick<DeliveryGoals, "build" | "pushImpact">>; /** * If this is provided, it can distinguish the UI instance. * Helps distinguish different SDMs during development. */ instanceMetadata?: ExtensionPackMetadata; /** * Optionally configure the rebase options for Code Transforms */ rebase?: RebaseOptions; /** * Optionally expose web endpoints * Defaults to true in local mode */ exposeWeb?: boolean; /** * Optionally secure the api endpoints * Defaults to false in local mode */ secureWeb?: boolean; } /** * Return an extension pack to add aspect support with the given aspects to an SDM. * If we're in local mode, expose analyzer commands and HTTP endpoints. */ export function aspectSupport(options: AspectSupportOptions): ExtensionPack { const scoringAspects: ScoredAspect[] = _.flatten( Object.getOwnPropertyNames(options.scorers || {}) .map(name => emitScoringAspect(name, toArray(options.scorers[name] || []), options.weightings))) .filter(a => !!a); const tagAspect = projectClassificationAspect({ name: "tagger", displayName: "tagger", }, ...toArray(options.taggers) || []); const aspects = [...toArray(options.aspects || []), ...scoringAspects, tagAspect]; // Default the two display methods with some sensible defaults aspects.forEach(a => { if (!a.toDisplayableFingerprint) { a.toDisplayableFingerprint = fp => JSON.stringify(fp.data); } if (!a.toDisplayableFingerprintName) { a.toDisplayableFingerprintName = fn => fn; } }); return { ...metadata(), configure: sdm => { const cfg = sdm.configuration; const analysisTracking = new AnalysisTracker(); if (isInLocalMode()) { // If we're in local mode, expose analyzer commands and // HTTP endpoints const analyzer = createAnalyzer( aspects, options.virtualProjectFinder || exports.DefaultVirtualProjectFinder); sdm.addCommand(analyzeGitHubByQueryCommandRegistration(analyzer, analysisTracking)); sdm.addCommand(analyzeGitHubOrganizationCommandRegistration(analyzer, analysisTracking)); sdm.addCommand(analyzeLocalCommandRegistration(analyzer, analysisTracking)); } else { // Add command to calculate fingerprints as part of the initial onboarding // job and on subsequent runs of "analyze org" sdm.addCommand(calculateFingerprintTask(sdm, aspects)); // Register all aspects on startup sdm.addStartupListener(registerAspects(sdm, aspects)); } // Add support for calculating aspects on push and computing delivery aspects // This is only possible in local mode if we have a fingerprint publisher, // as we can't send to Atomist (the default) if (!!options.goals && (!isInLocalMode() || !!options.publishFingerprints)) { if (!!options.goals.pushImpact) { // Add supporting for calculating fingerprints on every push sdm.addExtensionPacks(fingerprintSupport({ pushImpactGoal: options.goals.pushImpact as PushImpact, aspects, aspectsFactory: options.aspectsFactory, rebase: options.rebase, publishFingerprints: options.publishFingerprints, })); } aspects .filter(isDeliveryAspect) .filter(a => a.canRegister(sdm, options.goals)) .forEach(da => da.register(sdm, options.goals, options.publishFingerprints)); } const exposeWeb = options.exposeWeb !== undefined ? options.exposeWeb : isInLocalMode(); if (exposeWeb) { const { customizers, routesToSuggestOnStartup } = orgVisualizationEndpoints(sdmConfigClientFactory(cfg), cfg, analysisTracking, options, aspects); cfg.http.customizers.push(...customizers); routesToSuggestOnStartup.forEach(rtsos => { cfg.logging.banner.contributors.push(suggestRoute(rtsos)); }); } }, }; } function suggestRoute({ title, route }: { title: string, route: string }): (c: Configuration) => BannerSection { return cfg => ({ title, body: `http://localhost:${cfg.http.port}${route}`, }); } function orgVisualizationEndpoints(dbClientFactory: ClientFactory, configuration: Configuration, analysisTracking: AnalysisTracking, options: AspectSupportOptions, aspects: Aspect[]): { routesToSuggestOnStartup: Array<{ title: string, route: string }>, customizers: ExpressCustomizer[], } { const resultStore = analysisResultStore(dbClientFactory); const fingerprintClassificationsFound = _.flatten(aspects.filter(isClassificationAspect).map(ca => ca.classifierMetadata)); const scorerNames = Object.getOwnPropertyNames((options.scorers || {})); const aspectRegistry = new DefaultAspectRegistry({ idealStore: resultStore, problemStore: resultStore, aspects, undesirableUsageChecker: options.undesirableUsageChecker, scorers: toArray(options.inMemoryScorers || []).concat(scorerNames.map(exposeFingerprintScore)), workspaceScorers: options.workspaceScorers, scoreWeightings: options.weightings || DefaultScoreWeightings, configuration, }) .withTaggers(...toArray(options.inMemoryTaggers || [])) // Add in memory taggers for all classification fingerprints .withTaggers(...tagsFromClassificationFingerprints(...fingerprintClassificationsFound)); if (options.secureWeb === undefined) { options.secureWeb = !isInLocalMode(); } const aboutTheApi = api(resultStore, aspectRegistry, options.secureWeb); if (!isInLocalMode() && !options.exposeWeb) { return { routesToSuggestOnStartup: aboutTheApi.routesToSuggestOnStartup, customizers: [aboutTheApi.customizer], }; } const aboutStaticPages = addWebAppRoutes(aspectRegistry, resultStore, analysisTracking, configuration.http.client.factory, options.instanceMetadata || metadata()); return { routesToSuggestOnStartup: [...aboutStaticPages.routesToSuggestOnStartup, ...aboutTheApi.routesToSuggestOnStartup], customizers: [aboutStaticPages.customizer, aboutTheApi.customizer], }; }