Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance libyear output #33981

Merged
merged 9 commits into from
Feb 1, 2025
Merged
83 changes: 83 additions & 0 deletions lib/workers/repository/process/libyear.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('workers/repository/process/libyear', () => {
deps: [
{
depName: 'some/image',
datasource: 'docker',
currentVersion: '1.0.0',
updates: [{ newVersion: '2.0.0' }],
},
Expand All @@ -30,6 +31,7 @@ describe('workers/repository/process/libyear', () => {
deps: [
{
depName: 'dep1',
datasource: 'npm',
currentVersion: '0.1.0',
currentVersionTimestamp: '2019-07-01T00:00:00Z' as Timestamp,
updates: [
Expand All @@ -56,6 +58,7 @@ describe('workers/repository/process/libyear', () => {
{
depName: 'dep2',
currentVersion: '1.0.0',
datasource: 'rubygems',
currentVersionTimestamp: '2019-07-01T00:00:00Z' as Timestamp,
updates: [
{
Expand All @@ -67,6 +70,7 @@ describe('workers/repository/process/libyear', () => {
{
depName: 'dep3',
currentVersion: '1.0.0',
datasource: 'rubygems',
updates: [
{
newVersion: '2.0.0',
Expand All @@ -76,6 +80,8 @@ describe('workers/repository/process/libyear', () => {
},
{
depName: 'dep4',
datasource: 'rubygems',
currentValue: '1.0.0', // coverage
},
],
},
Expand All @@ -100,6 +106,83 @@ describe('workers/repository/process/libyear', () => {
},
// eslint-disable-next-line no-loss-of-precision
totalLibYears: 1.5027322404371585,
totalDepsCount: 5,
outdatedDepsCount: 4,
},
'Repository libYears',
);
});

it('de-duplicates if same dep found in different files', () => {
// there are three package files with the same dependency + version but mixed datasources
const packageFiles = {
npm: [
{
packageFile: 'folder1/package.json',
deps: [
{
depName: 'dep1',
currentVersion: '0.1.0',
datasource: 'npm',
currentVersionTimestamp: '2019-07-01T00:00:00Z' as Timestamp,
updates: [
{
newVersion: '1.0.0',
releaseTimestamp: '2020-07-01T00:00:00Z' as Timestamp,
},
],
},
],
},
{
packageFile: 'folder2/package.json',
deps: [
{
depName: 'dep1',
currentVersion: '0.1.0',
datasource: 'npm',
currentVersionTimestamp: '2019-07-01T00:00:00Z' as Timestamp,
updates: [
{
newVersion: '1.0.0',
releaseTimestamp: '2020-07-01T00:00:00Z' as Timestamp,
},
],
},
],
},
],
regex: [
{
packageFile: 'folder3/package.json',
deps: [
{
depName: 'dep1',
currentVersion: '0.1.0',
datsource: 'docker',
currentVersionTimestamp: '2019-07-01T00:00:00Z' as Timestamp,
updates: [
{
newVersion: '1.0.0',
releaseTimestamp: '2020-07-01T00:00:00Z' as Timestamp,
},
],
},
],
},
],
};
calculateLibYears(packageFiles);
expect(logger.logger.debug).toHaveBeenCalledWith(
{
managerLibYears: {
npm: 1,
regex: 1,
},
// eslint-disable-next-line no-loss-of-precision
totalLibYears: 2,
totalDepsCount: 2,
outdatedDepsCount: 2,
},
'Repository libYears',
);
Expand Down
132 changes: 101 additions & 31 deletions lib/workers/repository/process/libyear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,129 @@ import { DateTime } from 'luxon';
import { logger } from '../../../logger';
import type { PackageFile } from '../../../modules/manager/types';

interface DepInfo {
depName: string;
manager: string;
datasource: string;
version: string;
file: string;
outdated?: boolean;
libYear?: number;
}
export function calculateLibYears(
packageFiles?: Record<string, PackageFile[]>,
): void {
if (!packageFiles) {
return;
}
const managerLibYears: Record<string, number> = {};
const allDeps: DepInfo[] = [];
for (const [manager, files] of Object.entries(packageFiles)) {
for (const file of files) {
let fileLibYears = 0;
for (const dep of file.deps) {
const depInfo: DepInfo = {
depName: dep.depName!,
manager,
file: file.packageFile,
datasource: dep.datasource!,
version: (dep.currentVersion ?? dep.currentValue)!,
};

if (!dep.updates?.length) {
allDeps.push(depInfo);
continue;
}

depInfo.outdated = true;
if (!dep.currentVersionTimestamp) {
logger.debug(`No currentVersionTimestamp for ${dep.depName}`);
allDeps.push(depInfo);
continue;
}
// timestamps are in ISO format
const currentVersionDate = DateTime.fromISO(
dep.currentVersionTimestamp,
);

if (dep.updates?.length) {
for (const update of dep.updates) {
if (!update.releaseTimestamp) {
logger.debug(
`No releaseTimestamp for ${dep.depName} update to ${update.newVersion}`,
);
continue;
}
const releaseDate = DateTime.fromISO(update.releaseTimestamp);
const libYears = releaseDate.diff(
currentVersionDate,
'years',
).years;
if (libYears >= 0) {
update.libYears = libYears;
}
for (const update of dep.updates) {
if (!update.releaseTimestamp) {
logger.debug(
`No releaseTimestamp for ${dep.depName} update to ${update.newVersion}`,
);
continue;
}
const releaseDate = DateTime.fromISO(update.releaseTimestamp);
const libYears = releaseDate.diff(currentVersionDate, 'years').years;
if (libYears >= 0) {
update.libYears = libYears;
}
// Set the highest libYears for the dep
const depLibYears = Math.max(
...dep.updates.map((update) => update.libYears ?? 0),
0,
);
fileLibYears += depLibYears;
}
// Set the highest libYears for the dep
const depLibYears = Math.max(
...dep.updates.map((update) => update.libYears ?? 0),
0,
);
depInfo.libYear = depLibYears;
allDeps.push(depInfo);
}
managerLibYears[manager] ??= 0;
managerLibYears[manager] += fileLibYears;
}
}
// Sum up the libYears for the repo
let totalLibYears = 0;
for (const libYears of Object.values(managerLibYears)) {
totalLibYears += libYears;

const [totalDepsCount, outdatedDepsCount, totalLibYears] = getCounts(allDeps);
logger.debug(
{
managerLibYears: getManagerLibYears(allDeps),
totalLibYears,
totalDepsCount,
outdatedDepsCount,
},
'Repository libYears',
);
}

function getManagerLibYears(deps: DepInfo[]): Record<string, number> {
/** {manager : {depKey: libYear }} */
const managerLibYears: Record<string, Record<string, number>> = {};
for (const dep of deps) {
const depKey = `${dep.depName}@${dep.version}@${dep.datasource}`;
const manager = dep.manager;
managerLibYears[manager] ??= {};
if (dep.libYear) {
if (!managerLibYears[manager][depKey]) {
managerLibYears[manager][depKey] = dep.libYear;
}
}
}
logger.debug({ managerLibYears, totalLibYears }, 'Repository libYears');

const res: Record<string, number> = {};
for (const [manager, deps] of Object.entries(managerLibYears)) {
const managerLibYear = Object.values(deps).reduce((sum, curr) => {
return sum + curr;
}, 0);
res[manager] = managerLibYear;
}

return res;
}

function getCounts(deps: DepInfo[]): [number, number, number] {
const distinctDeps = new Set<string>();
let totalDepsCount = 0,
outdatedDepsCount = 0,
totalLibYears = 0;
for (const dep of deps) {
const depKey = `${dep.depName}@${dep.version}@${dep.datasource}`;
if (!distinctDeps.has(depKey)) {
if (dep.outdated) {
outdatedDepsCount++;
}
if (dep.libYear) {
totalLibYears += dep.libYear;
}

totalDepsCount++;
distinctDeps.add(depKey);
}
}

return [totalDepsCount, outdatedDepsCount, totalLibYears];
}