UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

309 lines (264 loc) 9.91 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 { 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()), }; } }