@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
344 lines (306 loc) • 12.7 kB
text/typescript
/*
* 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],
};
}