diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index 2ce51aa306a0d9..7fa394d4ebaa11 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -106,6 +106,7 @@ export interface LookupUpdate { releaseTimestamp?: any; newVersionAgeInDays?: number; registryUrl?: string; + libYears?: number; } /** @@ -144,6 +145,7 @@ export interface PackageDependency> digestOneAndOnly?: boolean; fixedVersion?: string; currentVersion?: string; + currentVersionTimestamp?: string; lockedVersion?: string; propSource?: string; registryUrls?: string[] | null; diff --git a/lib/workers/repository/process/extract-update.ts b/lib/workers/repository/process/extract-update.ts index 8cc0163d84d363..f2a42103ec8c52 100644 --- a/lib/workers/repository/process/extract-update.ts +++ b/lib/workers/repository/process/extract-update.ts @@ -13,6 +13,7 @@ import { extractAllDependencies } from '../extract'; import { generateFingerprintConfig } from '../extract/extract-fingerprint-config'; import { branchifyUpgrades } from '../updates/branchify'; import { fetchUpdates } from './fetch'; +import { calculateLibYears } from './libyear'; import { sortBranches } from './sort'; import { Vulnerabilities } from './vulnerabilities'; import type { WriteUpdateResult } from './write'; @@ -211,6 +212,7 @@ export async function lookup( ): Promise { await fetchVulnerabilities(config, packageFiles); await fetchUpdates(config, packageFiles); + calculateLibYears(packageFiles); const { branches, branchList } = await branchifyUpgrades( config, packageFiles, diff --git a/lib/workers/repository/process/libyear.spec.ts b/lib/workers/repository/process/libyear.spec.ts new file mode 100644 index 00000000000000..b5f4c376b831d9 --- /dev/null +++ b/lib/workers/repository/process/libyear.spec.ts @@ -0,0 +1,108 @@ +import { logger } from '../../../../test/util'; +import type { PackageFile } from '../../../modules/manager/types'; +import { calculateLibYears } from './libyear'; + +describe('workers/repository/process/libyear', () => { + describe('calculateLibYears', () => { + beforeEach(() => { + logger.logger.debug.mockClear(); + }); + + it('returns early if no packageFiles', () => { + calculateLibYears(undefined); + expect(logger.logger.debug).not.toHaveBeenCalled(); + }); + + it('calculates libYears', () => { + const packageFiles: Record = { + dockerfile: [ + { + packageFile: 'Dockerfile', + deps: [ + { + depName: 'some/image', + currentVersion: '1.0.0', + updates: [{ newVersion: '2.0.0' }], + }, + ], + }, + ], + npm: [ + { + packageFile: 'package.json', + deps: [ + { + depName: 'dep1', + currentVersion: '0.1.0', + currentVersionTimestamp: '2019-07-01T00:00:00Z', + updates: [ + { + newVersion: '1.0.0', + releaseTimestamp: '2020-01-01T00:00:00Z', + }, + { + newVersion: '2.0.0', + releaseTimestamp: '2020-07-01T00:00:00Z', + }, + { + newVersion: '3.0.0', + }, + ], + }, + ], + }, + ], + bundler: [ + { + packageFile: 'Gemfile', + deps: [ + { + depName: 'dep2', + currentVersion: '1.0.0', + currentVersionTimestamp: '2019-07-01T00:00:00Z', + updates: [ + { + newVersion: '2.0.0', + releaseTimestamp: '2020-01-01T00:00:00Z', + }, + ], + }, + { + depName: 'dep3', + currentVersion: '1.0.0', + updates: [ + { + newVersion: '2.0.0', + releaseTimestamp: '2020-01-01T00:00:00Z', + }, + ], + }, + { + depName: 'dep4', + }, + ], + }, + ], + }; + calculateLibYears(packageFiles); + expect(logger.logger.debug).toHaveBeenCalledWith( + 'No releaseTimestamp for some/image update to 2.0.0', + ); + expect(logger.logger.debug).toHaveBeenCalledWith( + 'No releaseTimestamp for dep1 update to 3.0.0', + ); + expect(logger.logger.debug).toHaveBeenCalledWith( + { + managerLibYears: { + bundler: 0.5027322404371585, + dockerfile: 0, + npm: 1, + }, + // eslint-disable-next-line no-loss-of-precision + totalLibYears: 1.5027322404371585, + }, + 'Repository libYears', + ); + }); + }); +}); diff --git a/lib/workers/repository/process/libyear.ts b/lib/workers/repository/process/libyear.ts new file mode 100644 index 00000000000000..1015ea32e6466c --- /dev/null +++ b/lib/workers/repository/process/libyear.ts @@ -0,0 +1,59 @@ +import { DateTime } from 'luxon'; +import { logger } from '../../../logger'; +import type { PackageFile } from '../../../modules/manager/types'; + +export function calculateLibYears( + packageFiles?: Record, +): void { + if (!packageFiles) { + return; + } + const managerLibYears: Record = {}; + for (const [manager, files] of Object.entries(packageFiles)) { + for (const file of files) { + let fileLibYears = 0; + for (const dep of file.deps) { + if (dep.updates?.length) { + for (const update of dep.updates) { + if (update.releaseTimestamp) { + if (dep.currentVersionTimestamp) { + // timestamps are in ISO format + const currentVersionDate = DateTime.fromISO( + dep.currentVersionTimestamp, + ); + const releaseDate = DateTime.fromISO(update.releaseTimestamp); + const libYears = releaseDate.diff( + currentVersionDate, + 'years', + ).years; + if (libYears >= 0) { + update.libYears = libYears; + } + } else { + logger.debug(`No currentVersionTimestamp for ${dep.depName}`); + } + } else { + logger.debug( + `No releaseTimestamp for ${dep.depName} update to ${update.newVersion}`, + ); + } + } + // Set the highest libYears for the dep + const depLibYears = Math.max( + ...dep.updates.map((update) => update.libYears ?? 0), + 0, + ); + fileLibYears += depLibYears; + } + } + managerLibYears[manager] ??= 0; + managerLibYears[manager] += fileLibYears; + } + } + // Sum up the libYears for the repo + const totalLibYears = Object.values(managerLibYears).reduce( + (acc, libYears) => acc + libYears, + 0, + ); + logger.debug({ managerLibYears, totalLibYears }, 'Repository libYears'); +}