@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
309 lines (264 loc) • 9.91 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 {
logger,
RepoRef,
} from "@atomist/automation-client";
import { SpiderResult } from "../offline/spider/Spider";
interface AnalysisTrackingRepo { description: string; url?: string; }
type AnalysisProgress = "Going" | "Stopped";
type RepoProgress = "Planned" | "Going" | "Stopped";
interface AnalysisForTracking {
description: string;
analysisKey: string;
progress: AnalysisProgress;
}
interface RepoForReporting {
description: string;
repoKey: string;
keptExisting: boolean;
progress: RepoProgress;
aspects: AspectForReporting[];
millisTaken?: number;
errorMessage?: string;
stackTrace?: string;
snapshotId?: string;
}
interface AnalysisForReporting {
description: string;
analysisKey: string;
progress: AnalysisProgress;
repos: RepoForReporting[];
error?: Error;
completedAt?: Date;
}
export interface AnalysisReport {
analyses: AnalysisForReporting[];
}
export interface AnalysisTracking {
startAnalysis(params: Pick<AnalysisForTracking, "description">): AnalysisBeingTracked;
report(): AnalysisReport;
}
export interface FailureDetails {
whileTryingTo: string; error?: Error; message?: string;
}
type WayToGetFingerprintsFromAnAspect = "extract" | "consolidate";
export interface AspectForReporting {
aspectName: string;
visible: boolean;
stage: WayToGetFingerprintsFromAnAspect;
millisTaken?: number;
error?: Error;
fingerprintsFound: number;
}
/**
* Track the calculation of fingerprints from one aspect on one repo snapshot
*/
export class AspectBeingTracked {
public readonly startedAt: Date;
public completedAt: Date | undefined;
public fingerprintsFound: number | undefined;
public failedWith: Error | undefined;
public moreFailures: Array<{ error: Error, furtherDescription: string }> = [];
constructor(readonly params: {
aspectName: string,
visible: boolean,
aboutToRun: WayToGetFingerprintsFromAnAspect,
}) {
this.startedAt = new Date();
}
public isThisYou(aspectName: string): boolean {
return this.params.aspectName === aspectName;
}
public failFingerprint(fp: { type: string, name: string }, error: Error): void {
this.moreFailures.push({ error, furtherDescription: "Failed on fingerprint: " + fp.name });
}
public completed(fingerprintsFound: number): void {
this.fingerprintsFound = fingerprintsFound;
this.completedAt = new Date();
}
public failed(err: Error): void {
this.failedWith = err;
this.completedAt = new Date();
}
public report(): AspectForReporting {
let error = this.failedWith;
if (!error && this.moreFailures.length === 1) {
error = this.moreFailures[0].error;
} else if (error && this.moreFailures.length > 0) {
const allErrorMessages = (error ? error.message + "\n" : "") +
this.moreFailures.map(f => f.furtherDescription + ": " + f.error.message).join("\n");
error = new Error(`multiple errors: ${allErrorMessages}`);
}
return {
aspectName: this.params.aspectName,
visible: this.params.visible,
stage: this.params.aboutToRun,
millisTaken: this.completedAt ? this.completedAt.getTime() - this.startedAt.getTime() : undefined,
fingerprintsFound: this.fingerprintsFound || 0,
error,
};
}
}
export interface AnalysisTrackingAspect {
name: string;
displayName: string | undefined;
}
export class RepoBeingTracked {
public repoRef: RepoRef | undefined = undefined;
public millisTaken: number | undefined;
public existingWasKept: boolean = false;
public persistedSnapshotId: string | undefined;
public failureDetails: FailureDetails | undefined = undefined;
public skipReason: string | undefined;
private analysisStartMillis: number | undefined;
private readonly aspects: AspectBeingTracked[] = [];
constructor(private readonly params: {
description: string;
url?: string;
repoKey: string;
}) {
}
public beganAnalysis(): void {
this.analysisStartMillis = new Date().getTime();
}
public setRepoRef(repoRef: RepoRef): void {
this.repoRef = repoRef;
}
public keptExisting(): void {
this.millisTaken = new Date().getTime() - this.analysisStartMillis;
this.existingWasKept = true;
}
public plan(aspect: AnalysisTrackingAspect, aboutToRun: WayToGetFingerprintsFromAnAspect): AspectBeingTracked {
const newAspect = new AspectBeingTracked({
aspectName: aspect.name,
visible: !!aspect.displayName, // this determines whether the aspect will display on the Insights page
aboutToRun,
});
this.aspects.push(newAspect);
return newAspect;
}
public failed(failureDetails: FailureDetails): void {
this.failureDetails = failureDetails;
this.millisTaken = this.millisTaken = new Date().getTime() - this.analysisStartMillis;
}
public failFingerprint(fp: { type: string, name: string }, error: Error): void {
const a = this.aspects.find(n => n.isThisYou(fp.type));
if (!a) {
logger.warn("Did not find aspect: " + fp.type);
return;
}
a.failFingerprint(fp, error);
}
public skipped(skipReason: string): void {
this.skipReason = skipReason || "unspecified reason";
this.millisTaken = this.millisTaken = new Date().getTime() - this.analysisStartMillis;
}
public persisted(snapshotId: string): void {
this.persistedSnapshotId = snapshotId;
this.millisTaken = this.millisTaken = new Date().getTime() - this.analysisStartMillis;
}
public report(): RepoForReporting {
const isGoing = !!this.analysisStartMillis;
const isDone = this.existingWasKept || this.persistedSnapshotId || this.failureDetails || this.skipReason;
const errorFields = !this.failureDetails ? {} : {
errorMessage: `Failed while trying to ${this.failureDetails.whileTryingTo}\n${this.failureDetails.message || ""}`,
stackTrace: this.failureDetails.error ? this.failureDetails.error.stack : undefined,
};
return {
...this.params,
progress: isDone ? "Stopped" : isGoing ? "Going" : "Planned",
keptExisting: this.existingWasKept,
millisTaken: this.millisTaken,
snapshotId: this.persistedSnapshotId,
aspects: this.aspects.map(a => a.report()),
...errorFields,
};
}
public spiderResult(): SpiderResult {
if (!this.repoRef) {
throw new Error("Can't return a SpiderResult until repoRef is set");
}
return {
repositoriesDetected: 1,
failed: this.failureDetails ? [{
repoUrl: this.repoRef.url,
whileTryingTo: this.failureDetails.whileTryingTo,
message: this.failureDetails.error ? this.failureDetails.error.message : this.failureDetails.message,
}] : [],
keptExisting: this.existingWasKept ? [this.repoRef.url] : [],
persistedAnalyses: this.persistedSnapshotId ? [this.repoRef.url] : [],
millisTaken: this.millisTaken,
};
}
}
// make the interface later
export class AnalysisBeingTracked {
public error?: Error;
public completedAt: Date | undefined;
private readonly repos: RepoBeingTracked[] = [];
constructor(public readonly me: AnalysisForTracking) {
}
private repoCount: number = 0;
public plan(repo: AnalysisTrackingRepo): RepoBeingTracked {
const newRepo = new RepoBeingTracked({
...repo,
repoKey: this.me.analysisKey + "/repo#" + this.repoCount++,
});
this.repos.push(newRepo);
return newRepo;
}
public stop(): void {
this.completedAt = new Date();
this.me.progress = "Stopped";
}
public failed(error: Error): void {
this.error = error;
this.me.progress = "Stopped";
}
public report(): AnalysisForReporting {
return {
...this.me,
error: this.error,
repos: this.repos.map(s => s.report()),
completedAt: this.completedAt,
};
}
}
/**
* Track analyses for display of status on a page.
* You want exactly one of these in your SDM.
*/
export class AnalysisTracker implements AnalysisTracking {
private counter: number = 1;
private readonly analyses: AnalysisBeingTracked[] = [];
// is there an "unpick" ?
public startAnalysis(params: Pick<AnalysisForTracking, "description">): AnalysisBeingTracked {
const analysisId = "analysis#" + this.counter++;
const newAnalysis: AnalysisBeingTracked = new AnalysisBeingTracked({
...params,
analysisKey: analysisId,
progress: "Going",
});
this.analyses.push(newAnalysis);
return newAnalysis;
}
public report(): AnalysisReport {
return {
analyses: this.analyses.map(a => a.report()),
};
}
}