import { maxBy } from "lodash" import { Runner } from "./runner" import { YamlConfig } from "../config/config" import { WorkflowReport, TestReport } from "../analyzer/analyzer" import { CompositExporter } from "../exporter/exporter" import { JenkinsClient } from "../client/jenkins_client" import { JenkinsAnalyzer } from "../analyzer/jenkins_analyzer" import { JenkinsConfig, JenkinsConfigJob, parseConfig } from "../config/jenkins_config" import { LastRunStore } from "../last_run_store" import { CustomReportCollection, createCustomReportCollection } from "../custom_report_collection" import { failure, Result, success } from "../result" import { ArgumentOptions } from "../arg_options" import { Logger } from "tslog" export class JenkinsRunner implements Runner { service: string = 'jenkins' client?: JenkinsClient analyzer?: JenkinsAnalyzer config?: JenkinsConfig store?: LastRunStore logger: Logger constructor(logger: Logger, public yamlConfig: YamlConfig, public options: ArgumentOptions) { this.config = parseConfig(yamlConfig) this.logger = logger.getChildLogger({ name: JenkinsRunner.name, instanceName: this.service }) if (!this.config) return const JENKINS_USER = process.env['JENKINS_USER'] const JENKINS_TOKEN = process.env['JENKINS_TOKEN'] this.analyzer = new JenkinsAnalyzer(this.config.baseUrl) this.client = new JenkinsClient(this.config.baseUrl, this.logger, options, JENKINS_USER, JENKINS_TOKEN) } private setRepoLastRun(jobname: string, reports: WorkflowReport[]) { const lastRunReport = maxBy(reports, 'buildNumber') if (lastRunReport) { this.store?.setLastRun(jobname, lastRunReport.buildNumber) } } async run (): Promise<Result<unknown, Error>> { let result: Result<unknown, Error> = success(this.service) if (!this.config) return failure(new Error('this.config must not be undefined')) if (!this.client) return failure(new Error('this.client must not be undefined')) if (!this.analyzer) return failure(new Error('this.analyzer must not be undefined')) this.store = await LastRunStore.init(this.logger, this.options, this.service, this.config.lastRunStore) const jobs = await this.getJobs() let workflowReports: WorkflowReport[] = [] let testReports: TestReport[] = [] const customReportCollection = new CustomReportCollection() for (const job of jobs) { this.logger.info(`Fetching ${this.service} - ${job.name} ...`) const jobReports: WorkflowReport[] = [] let jobTestReports: TestReport[] = [] try { const lastRunId = this.store.getLastRun(job.name) const runs = await this.client.fetchJobRuns(job.name, lastRunId) if (runs.length < 1) { this.logger.warn(`SKIP ${job.name}: it does not have any runs data for some reason`) continue } for (const run of runs) { // Fetch data const build = await this.client.fetchBuild(job.name, Number(run.id)) const tests = await this.client.fetchTests(build, job.testGlob) const customReportArtifacts = await this.client.fetchCustomReports(build, job.customReports) // Create report const report = this.analyzer.createWorkflowReport(job.name, run, build) const testReports = await this.analyzer.createTestReports(report, tests) const runCustomReportCollection = await createCustomReportCollection(report, customReportArtifacts) // Aggregate jobReports.push(report) jobTestReports = jobTestReports.concat(testReports) customReportCollection.aggregate(runCustomReportCollection) } this.setRepoLastRun(job.name, jobReports) workflowReports = workflowReports.concat(jobReports) testReports = testReports.concat(jobTestReports) } catch (error) { const errorMessage = `Some error raised in '${job.name}', so it skipped.` this.logger.error(errorMessage) result = failure(new Error(errorMessage)) continue } } this.logger.info(`Exporting ${this.service} workflow reports ...`) const exporter = new CompositExporter(this.logger, this.options, this.service, this.config.exporter) await exporter.exportWorkflowReports(workflowReports) await exporter.exportTestReports(testReports) await exporter.exportCustomReports(customReportCollection) this.store.save() this.logger.info(`Done execute '${this.service}'. status: ${result.type}`) return result } private async getJobs(): Promise<JenkinsConfigJob[]> { if (!this.config) return [] if (!this.client) return [] const allJobs = await this.client.fetchJobs() const allJobMap = new Map(allJobs.map((job) => [job.name, job])) const configJobs = this.config.jobs.filter((job) => allJobMap.get(job.name)) const otherJobs: JenkinsConfigJob[] = [] if (this.config.correctAllJobs) { const notConfigJobs = allJobs.filter((job) => { return configJobs.find((configJob) => configJob.name === job.name) === undefined }) const buildRespones = notConfigJobs.map((job) => { return { jobName: job.name, resultPromise: this.client!.fetchLastBuild(job.name) .then((res) => success(res)) .catch((error) => Promise.resolve(failure(error))) } }) const stallDays = this.config.correctAllJobs.filterLastBuildDay ?? 30 const now = Date.now() const thresholdTimestamp = now - stallDays * 24 * 60 * 60 * 1000 for (const { jobName, resultPromise } of buildRespones) { const result = await resultPromise if (result.isFailure()) { this.logger.warn(`SKIP ${jobName}: Can not fetch lastBuild number.`) continue } if (result.isSuccess() && result.value && result.value.timestamp >= thresholdTimestamp) { otherJobs.push({ name: jobName, testGlob: [], customReports: [] }) } } } return [...configJobs, ...otherJobs] } }