diff --git a/data/debian-distro-info.json b/data/debian-distro-info.json new file mode 100644 index 0000000000000000000000000000000000000000..29e555c654d344c0c9ea8fce371824aa4da536d0 --- /dev/null +++ b/data/debian-distro-info.json @@ -0,0 +1,133 @@ +{ + "v1.1": { + "codename": "Buzz", + "series": "buzz", + "created": "1993-08-16", + "release": "1996-06-17", + "eol": "1997-06-05" + }, + "v1.2": { + "codename": "Rex", + "series": "rex", + "created": "1996-06-17", + "release": "1996-12-12", + "eol": "1998-06-05" + }, + "v1.3": { + "codename": "Bo", + "series": "bo", + "created": "1996-12-12", + "release": "1997-06-05", + "eol": "1999-03-09" + }, + "v2": { + "codename": "Hamm", + "series": "hamm", + "created": "1997-06-05", + "release": "1998-07-24", + "eol": "2000-03-09" + }, + "v2.1": { + "codename": "Slink", + "series": "slink", + "created": "1998-07-24", + "release": "1999-03-09", + "eol": "2000-10-30" + }, + "v2.2": { + "codename": "Potato", + "series": "potato", + "created": "1999-03-09", + "release": "2000-08-15", + "eol": "2003-07-30" + }, + "v3": { + "codename": "Woody", + "series": "woody", + "created": "2000-08-15", + "release": "2002-07-19", + "eol": "2006-06-30" + }, + "v3.1": { + "codename": "Sarge", + "series": "sarge", + "created": "2002-07-19", + "release": "2005-06-06", + "eol": "2008-03-30" + }, + "v4": { + "codename": "Etch", + "series": "etch", + "created": "2005-06-06", + "release": "2007-04-08", + "eol": "2010-02-15" + }, + "v5": { + "codename": "Lenny", + "series": "lenny", + "created": "2007-04-08", + "release": "2009-02-14", + "eol": "2012-02-06" + }, + "v6": { + "codename": "Squeeze", + "series": "squeeze", + "created": "2009-02-14", + "release": "2011-02-06", + "eol": "2014-05-31", + "eol_lts": "2016-02-29" + }, + "v7": { + "codename": "Wheezy", + "series": "wheezy", + "created": "2011-02-06", + "release": "2013-05-04", + "eol": "2016-04-26", + "eol_lts": "2018-05-31", + "eol_elts": "2020-06-30" + }, + "v8": { + "codename": "Jessie", + "series": "jessie", + "created": "2013-05-04", + "release": "2015-04-25", + "eol": "2018-06-17", + "eol_lts": "2020-06-30", + "eol_elts": "2022-06-30" + }, + "v9": { + "codename": "Stretch", + "series": "stretch", + "created": "2015-04-25", + "release": "2017-06-17", + "eol": "2020-07-06", + "eol_lts": "2022-06-30", + "eol_elts": "2024-06-30" + }, + "v10": { + "codename": "Buster", + "series": "buster", + "created": "2017-06-17", + "release": "2019-07-06", + "eol": "2022-08-14", + "eol_lts": "2024-06-30", + "eol_elts": "2026-06-30" + }, + "v11": { + "codename": "Bullseye", + "series": "bullseye", + "created": "2019-07-06", + "release": "2021-08-14", + "eol": "2024-08-14" + }, + "v12": { + "codename": "Bookworm", + "series": "bookworm", + "created": "2021-08-14" + }, + "v13": { + "codename": "Trixie", + "series": "trixie", + "created": "2023-08-01" + } +} diff --git a/data/ubuntu-distro-info.json b/data/ubuntu-distro-info.json index d5db78682c4000452650065548b5e416f6639bc3..6c8ff25b2e89969b09f36d82ae16a5bacd134e55 100644 --- a/data/ubuntu-distro-info.json +++ b/data/ubuntu-distro-info.json @@ -265,5 +265,12 @@ "eol": "2027-04-21", "eol_server": "2027-04-21", "eol_esm": "2032-04-21" + }, + "v22.10": { + "codename": "Kinetic Kudu", + "series": "kinetic", + "created": "2022-04-21", + "release": "2022-10-20", + "eol": "2023-07-20" } } diff --git a/docs/usage/docker.md b/docs/usage/docker.md index c891704e7e113dd8ba380c83f31f25c89912640b..896f51f5c476fa54b72a6c87a69cd1cb5f05b112 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -132,12 +132,30 @@ For example, Renovate will offer to upgrade the following `Dockerfile` layer: FROM ubuntu:yakkety ``` -To +To: ```dockerfile FROM ubuntu:focal ``` +### Debian codenames + +Renovate understands [Debian release code names and rolling updates schedule](https://wiki.debian.org/DebianReleases) and will offer upgrades to the latest stable release (e.g. from `debian:stretch` to `debian:bullseye`). + +For this to work the codename must be in lowercase. + +For example, Renovate will offer to upgrade the following `Dockerfile` layer: + +```dockerfile +FROM debian:buster +``` + +To: + +```dockerfile +FROM debian:bullseye +``` + ## Configuring/Disabling If you wish to make changes that apply to all Docker managers, then add them to the `docker` config object. diff --git a/lib/modules/manager/dockerfile/extract.spec.ts b/lib/modules/manager/dockerfile/extract.spec.ts index 9f64b74af4d33d76eaafd00453205609e0da0779..81c8a91e4e464f923204d65cec2d48a8ee0b2d7f 100644 --- a/lib/modules/manager/dockerfile/extract.spec.ts +++ b/lib/modules/manager/dockerfile/extract.spec.ts @@ -594,6 +594,41 @@ describe('modules/manager/dockerfile/extract', () => { `); }); + it('handles debian with codename', () => { + const res = extractPackageFile('FROM debian:buster\n').deps; + expect(res).toEqual([ + { + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: 'buster', + datasource: 'docker', + depName: 'debian', + depType: 'final', + replaceString: 'debian:buster', + versioning: 'debian', + }, + ]); + }); + + it('handles debian with prefixes', () => { + const res = extractPackageFile('FROM amd64/debian:10\n').deps; + expect(res).toEqual([ + { + autoReplaceStringTemplate: + '{{packageName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '10', + datasource: 'docker', + depName: 'debian', + depType: 'final', + packageName: 'amd64/debian', + replaceString: 'amd64/debian:10', + versioning: 'debian', + }, + ]); + }); + it('handles prefixes', () => { const res = extractPackageFile('FROM amd64/ubuntu:18.04\n').deps; expect(res).toMatchInlineSnapshot(` diff --git a/lib/modules/manager/dockerfile/extract.ts b/lib/modules/manager/dockerfile/extract.ts index ab77bc144fefecb2e282fde287d58b6b79faacd6..6dc2b0d869cd9aeefd2f127650df28758353a6e3 100644 --- a/lib/modules/manager/dockerfile/extract.ts +++ b/lib/modules/manager/dockerfile/extract.ts @@ -2,6 +2,7 @@ import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { DockerDatasource } from '../../datasource/docker'; +import * as debianVersioning from '../../versioning/debian'; import * as ubuntuVersioning from '../../versioning/ubuntu'; import type { PackageDependency, PackageFile } from '../types'; @@ -140,6 +141,10 @@ export function getDep( dep.versioning = ubuntuVersioning.id; } + if (dep.depName === 'debian') { + dep.versioning = debianVersioning.id; + } + // Don't display quay.io ports if (dep.depName && quayRegex.test(dep.depName)) { const depName = dep.depName.replace(quayRegex, 'quay.io'); diff --git a/lib/modules/versioning/api.ts b/lib/modules/versioning/api.ts index a76a544750e28a466c581fefeccc06ccffde9ed5..5edcdcccc857111d38d3465fb6097466e35296f3 100644 --- a/lib/modules/versioning/api.ts +++ b/lib/modules/versioning/api.ts @@ -2,6 +2,7 @@ import * as amazonMachineImage from './aws-machine-image'; import * as cargo from './cargo'; import * as composer from './composer'; import * as conan from './conan'; +import * as debian from './debian'; import * as docker from './docker'; import * as git from './git'; import * as gradle from './gradle'; @@ -32,6 +33,7 @@ api.set(amazonMachineImage.id, amazonMachineImage.api); api.set('cargo', cargo.api); api.set('composer', composer.api); api.set('conan', conan.api); +api.set('debian', debian.api); api.set('docker', docker.api); api.set('git', git.api); api.set('gradle', gradle.api); diff --git a/lib/modules/versioning/debian/common.ts b/lib/modules/versioning/debian/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..6341489abcc3e02080fe1772893ce62b58a39c22 --- /dev/null +++ b/lib/modules/versioning/debian/common.ts @@ -0,0 +1,77 @@ +import { DateTime } from 'luxon'; +import { logger } from '../../../logger'; +import type { DistroInfo, DistroInfoRecordWithVersion } from '../distro'; + +const refreshInterval = { days: 1 }; + +export class RollingReleasesData { + private ltsToVer = new Map<string, DistroInfoRecordWithVersion>(); + private verToLts = new Map<string, DistroInfoRecordWithVersion>(); + private timestamp = DateTime.fromMillis(0).toUTC(); // start of epoch + private distroInfo: DistroInfo; + + constructor(distroInfo: DistroInfo) { + this.distroInfo = distroInfo; + } + + getVersionByLts(input: string): string { + this.build(); + const schedule = this.ltsToVer.get(input); + if (schedule) { + return schedule.version; + } + return input; + } + + getLtsByVersion(input: string): string { + this.build(); + const di = this.verToLts.get(input); + if (di) { + return di.series; + } + return input; + } + + has(version: string): boolean { + this.build(); + return this.ltsToVer.has(version); + } + + schedule(version: string): DistroInfoRecordWithVersion | undefined { + this.build(); + let schedule: DistroInfoRecordWithVersion | undefined = undefined; + if (this.verToLts.has(version)) { + schedule = this.verToLts.get(version); + } + if (this.ltsToVer.has(version)) { + schedule = this.ltsToVer.get(version); + } + return schedule; + } + + private build(): void { + const now = DateTime.now().toUTC(); + if (now < this.timestamp.plus(refreshInterval)) { + return; + } + logger.debug('RollingReleasesData - data written'); + this.timestamp = now; + for (let i = 0; i < 3; i++) { + const di = this.distroInfo.getNLatest(i); + + // istanbul ignore if: should never happen + if (!di) { + return; + } + + let prefix = ''; + for (let j = 0; j < i; j++) { + prefix += 'old'; + } + di.series = prefix + 'stable'; + + this.ltsToVer.set(di.series, di); + this.verToLts.set(di.version, di); + } + } +} diff --git a/lib/modules/versioning/debian/index.spec.ts b/lib/modules/versioning/debian/index.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5406dc4c6d07557279148af2348ed6be367042e --- /dev/null +++ b/lib/modules/versioning/debian/index.spec.ts @@ -0,0 +1,428 @@ +import { DateTime, Settings } from 'luxon'; +import { logger } from '../../../logger'; +import { DebianVersioningApi } from '.'; + +describe('modules/versioning/debian/index', () => { + const dt = DateTime.fromISO('2022-04-20'); + + const spy = jest.spyOn(Settings, 'now'); + const debian = new DebianVersioningApi(); + + beforeEach(() => { + spy.mockReturnValue(dt.valueOf()); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each` + version | expected + ${undefined} | ${false} + ${null} | ${false} + ${''} | ${false} + ${'buzz'} | ${true} + ${'rex'} | ${true} + ${'bo'} | ${true} + ${'hamm'} | ${true} + ${'slink'} | ${true} + ${'potato'} | ${true} + ${'woody'} | ${true} + ${'sarge'} | ${true} + ${'etch'} | ${true} + ${'lenny'} | ${true} + ${'squeeze'} | ${true} + ${'wheezy'} | ${true} + ${'jessie'} | ${true} + ${'stretch'} | ${true} + ${'buster'} | ${true} + ${'Buster'} | ${false} + ${'bullseye'} | ${true} + ${'bookworm'} | ${false} + ${'trixie'} | ${false} + ${'sid'} | ${false} + ${'1.1'} | ${true} + ${'1.2'} | ${true} + ${'1.3'} | ${true} + ${'2'} | ${true} + ${'2.1'} | ${true} + ${'2.2'} | ${true} + ${'3'} | ${true} + ${'4'} | ${true} + ${'5'} | ${true} + ${'6'} | ${true} + ${'7'} | ${true} + ${'8'} | ${true} + ${'9'} | ${true} + ${'10'} | ${true} + ${'10-slim'} | ${false} + ${'11'} | ${true} + ${'12'} | ${false} + ${'13'} | ${false} + ${'sid'} | ${false} + ${'stable'} | ${true} + ${'oldstable'} | ${true} + ${'oldoldstable'} | ${true} + ${'experimental'} | ${false} + `('isValid("$version") === $expected', ({ version, expected }) => { + spy.mockReturnValue(dt.valueOf()); + expect(debian.isValid(version)).toBe(expected); + }); + + it.each` + version | range | expected + ${undefined} | ${undefined} | ${false} + ${null} | ${undefined} | ${false} + ${''} | ${undefined} | ${false} + ${'7'} | ${undefined} | ${true} + ${'11'} | ${undefined} | ${true} + ${'12'} | ${undefined} | ${false} + ${'stable'} | ${undefined} | ${true} + ${'oldstable'} | ${undefined} | ${true} + ${'oldoldstable'} | ${undefined} | ${true} + ${'wheezy'} | ${undefined} | ${true} + ${'bullseye'} | ${undefined} | ${true} + ${'bookworm'} | ${undefined} | ${false} + `( + 'isCompatible("$version") === $expected', + ({ version, range, expected }) => { + expect(debian.isCompatible(version, range)).toBe(expected); + } + ); + + it.each` + version | expected + ${undefined} | ${false} + ${null} | ${false} + ${''} | ${false} + ${'6'} | ${true} + ${'>=6'} | ${false} + `('isSingleVersion("$version") === $expected', ({ version, expected }) => { + expect(debian.isSingleVersion(version)).toBe(expected); + }); + + it.each` + version | expected + ${undefined} | ${false} + ${null} | ${false} + ${''} | ${false} + ${'buzz'} | ${false} + ${'rex'} | ${false} + ${'bo'} | ${false} + ${'hamm'} | ${false} + ${'slink'} | ${false} + ${'potato'} | ${false} + ${'woody'} | ${false} + ${'sarge'} | ${false} + ${'etch'} | ${false} + ${'lenny'} | ${false} + ${'squeeze'} | ${false} + ${'wheezy'} | ${false} + ${'jessie'} | ${false} + ${'stretch'} | ${true} + ${'buster'} | ${true} + ${'bullseye'} | ${true} + ${'bookworm'} | ${false} + ${'trixie'} | ${false} + ${'sid'} | ${false} + ${'1.1'} | ${false} + ${'1.2'} | ${false} + ${'1.3'} | ${false} + ${'2'} | ${false} + ${'2.1'} | ${false} + ${'2.2'} | ${false} + ${'3'} | ${false} + ${'4'} | ${false} + ${'5'} | ${false} + ${'6'} | ${false} + ${'7'} | ${false} + ${'8'} | ${false} + ${'9'} | ${true} + ${'10'} | ${true} + ${'11'} | ${true} + ${'12'} | ${false} + ${'13'} | ${false} + ${'sid'} | ${false} + ${'experimental'} | ${false} + ${'stable'} | ${true} + ${'oldstable'} | ${true} + ${'oldoldstable'} | ${true} + `('isStable("$version") === $expected', ({ version, expected }) => { + spy.mockReturnValue(dt.valueOf()); + expect(debian.isStable(version)).toBe(expected); + }); + + it.each` + version + ${'10'} + ${'11'} + ${'12'} + ${'13'} + ${'sid'} + ${'experimental'} + ${'stable'} + ${'oldstable'} + ${'oldoldstable'} + `( + 'ensures that rolling release is not refreshed within frame time window', + ({ version, expected }) => { + debian.isStable(version); + expect(logger.debug).toHaveBeenCalledTimes(0); + } + ); + + it('checks runtime date handling & refresh rolling release data', () => { + const future = DateTime.now().toUTC().plus({ year: 3 }).valueOf(); + const past = DateTime.fromISO('2019-08-06', { zone: 'UTC' }).valueOf(); + spy.mockReturnValue(past); + expect(debian.isStable('buster')).toBeTrue(); + spy.mockReturnValue(future); + expect(debian.isStable('buster')).toBeFalse(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenCalledWith( + 'RollingReleasesData - data written' + ); + }); + + it.each` + version | expected + ${undefined} | ${false} + ${null} | ${false} + ${''} | ${false} + ${'02.10'} | ${false} + ${'04.10'} | ${false} + ${'05.04'} | ${false} + ${'6.06'} | ${false} + ${'8.04'} | ${false} + ${'9.04'} | ${false} + ${'buzz'} | ${true} + ${'rex'} | ${true} + ${'bo'} | ${true} + ${'hamm'} | ${true} + ${'slink'} | ${true} + ${'potato'} | ${true} + ${'woody'} | ${true} + ${'sarge'} | ${true} + ${'etch'} | ${true} + ${'lenny'} | ${true} + ${'squeeze'} | ${true} + ${'wheezy'} | ${true} + ${'jessie'} | ${true} + ${'stretch'} | ${true} + ${'buster'} | ${true} + ${'bullseye'} | ${true} + ${'bookworm'} | ${false} + ${'trixie'} | ${false} + ${'sid'} | ${false} + ${'1.1'} | ${true} + ${'1.2'} | ${true} + ${'1.3'} | ${true} + ${'2'} | ${true} + ${'2.1'} | ${true} + ${'2.2'} | ${true} + ${'3'} | ${true} + ${'4'} | ${true} + ${'5'} | ${true} + ${'6'} | ${true} + ${'7'} | ${true} + ${'8'} | ${true} + ${'9'} | ${true} + ${'10'} | ${true} + ${'11'} | ${true} + ${'12'} | ${false} + ${'13'} | ${false} + ${'sid'} | ${false} + ${'experimental'} | ${false} + ${'Bookworm'} | ${false} + ${'Sid'} | ${false} + ${'Potato-'} | ${false} + ${'Woody'} | ${false} + ${'stable'} | ${true} + ${'oldstable'} | ${true} + ${'oldoldstable'} | ${true} + `('isVersion("$version") === $expected', ({ version, expected }) => { + expect(debian.isVersion(version)).toBe(expected); + }); + + it.each` + version | major | minor | patch + ${undefined} | ${null} | ${null} | ${null} + ${null} | ${null} | ${null} | ${null} + ${''} | ${null} | ${null} | ${null} + ${'42'} | ${null} | ${null} | ${null} + ${'2020.04'} | ${null} | ${null} | ${null} + ${'3.1'} | ${3} | ${1} | ${null} + ${'1.1'} | ${1} | ${1} | ${null} + ${'7'} | ${7} | ${null} | ${null} + ${'8'} | ${8} | ${null} | ${null} + ${'9'} | ${9} | ${null} | ${null} + ${'10'} | ${10} | ${null} | ${null} + ${'oldoldstable'} | ${9} | ${null} | ${null} + ${'oldstable'} | ${10} | ${null} | ${null} + ${'stable'} | ${11} | ${null} | ${null} + `( + 'getMajor, getMinor, getPatch for "$version"', + ({ version, major, minor, patch }) => { + expect(debian.getMajor(version)).toBe(major); + expect(debian.getMinor(version)).toBe(minor); + expect(debian.getPatch(version)).toBe(patch); + } + ); + + it.each` + a | b | expected + ${'woody'} | ${'sarge'} | ${false} + ${'lenny'} | ${'3'} | ${false} + ${'lenny'} | ${'5'} | ${true} + ${'squeeze'} | ${'6'} | ${true} + ${'10'} | ${'buster'} | ${true} + ${'6'} | ${'squeeze'} | ${true} + ${'buster'} | ${'10'} | ${true} + ${'oldoldstable'} | ${'9'} | ${true} + ${'oldstable'} | ${'10'} | ${true} + ${'stable'} | ${'11'} | ${true} + ${'9'} | ${'oldoldstable'} | ${true} + ${'10'} | ${'oldstable'} | ${true} + ${'11'} | ${'stable'} | ${true} + `('equals($a, $b) === $expected', ({ a, b, expected }) => { + expect(debian.equals(a, b)).toBe(expected); + }); + + it.each` + a | b | expected + ${'5'} | ${'6'} | ${false} + ${'6'} | ${'5'} | ${true} + ${'5'} | ${'10'} | ${false} + ${'11'} | ${'10'} | ${true} + ${'5'} | ${'6'} | ${false} + ${'11'} | ${'1.1'} | ${true} + ${'xxx'} | ${'yyy'} | ${true} + ${'yyy'} | ${'xxx'} | ${true} + ${'lenny'} | ${'squeeze'} | ${false} + ${'squeeze'} | ${'lenny'} | ${true} + ${'lenny'} | ${'buster'} | ${false} + ${'bookworm'} | ${'etch'} | ${true} + ${'sarge'} | ${'bo'} | ${true} + ${'bullseye'} | ${'rex'} | ${true} + ${'buzz'} | ${'jessie'} | ${false} + ${'oldoldstable'} | ${'8'} | ${true} + ${'oldstable'} | ${'oldoldstable'} | ${true} + ${'stable'} | ${'oldstable'} | ${true} + ${'11'} | ${'oldoldstable'} | ${true} + ${'10'} | ${'oldstable'} | ${false} + ${'9'} | ${'stable'} | ${false} + `('isGreaterThan("$a", "$b") === $expected', ({ a, b, expected }) => { + expect(debian.isGreaterThan(a, b)).toBe(expected); + }); + + it.each` + versions | range | expected + ${['8', '9', '10', '11']} | ${'2020.04'} | ${null} + ${['8', '9', '10', '11']} | ${'foobar'} | ${null} + ${['8', '9', '10', '11']} | ${'11'} | ${'11'} + ${['8', '9', '10', '11']} | ${'10'} | ${'10'} + ${['8', '9', '10', '11']} | ${'4'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'2020.04'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'foobar'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'bullseye'} | ${'bullseye'} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'buster'} | ${'buster'} + ${['jessie', 'stretch', 'buster', 'stable']} | ${'stable'} | ${'stable'} + ${['jessie', 'stretch', 'oldstable', 'bullseye']} | ${'buster'} | ${'oldstable'} + ${['jessie', 'oldoldstable', 'buster', 'bullseye']} | ${'warty'} | ${null} + `( + 'getSatisfyingVersion($versions, "$range") === "$expected"', + ({ versions, range, expected }) => { + expect(debian.getSatisfyingVersion(versions, range)).toBe(expected); + } + ); + + it.each` + versions | range | expected + ${['8', '9', '10', '11']} | ${'2020.04'} | ${null} + ${['8', '9', '10', '11']} | ${'foobar'} | ${null} + ${['8', '9', '10', '11']} | ${'11'} | ${'11'} + ${['8', '9', '10', '11']} | ${'10'} | ${'10'} + ${['8', '9', '10', '11']} | ${'4'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'2020.04'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'foobar'} | ${null} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'bullseye'} | ${'bullseye'} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'buster'} | ${'buster'} + ${['jessie', 'stretch', 'buster', 'bullseye']} | ${'warty'} | ${null} + ${['jessie', 'stretch', 'buster', 'stable']} | ${'stable'} | ${'stable'} + ${['jessie', 'stretch', 'oldstable', 'bullseye']} | ${'buster'} | ${'oldstable'} + ${['jessie', 'oldoldstable', 'buster', 'bullseye']} | ${'warty'} | ${null} + `( + 'minSatisfyingVersion($versions, "$range") === "$expected"', + ({ versions, range, expected }) => { + expect(debian.minSatisfyingVersion(versions, range)).toBe(expected); + } + ); + + it.each` + currentValue | rangeStrategy | currentVersion | newVersion | expected + ${undefined} | ${undefined} | ${undefined} | ${'foobar'} | ${'foobar'} + ${'stretch'} | ${undefined} | ${undefined} | ${'11'} | ${'bullseye'} + ${'stretch'} | ${undefined} | ${undefined} | ${'bullseye'} | ${'bullseye'} + ${'stretch'} | ${undefined} | ${undefined} | ${'stable'} | ${'bullseye'} + ${'9'} | ${undefined} | ${undefined} | ${'11'} | ${'11'} + ${'oldoldstable'} | ${undefined} | ${undefined} | ${'11'} | ${'stable'} + ${'oldstable'} | ${undefined} | ${undefined} | ${'11'} | ${'stable'} + ${'9'} | ${undefined} | ${undefined} | ${'stable'} | ${'11'} + ${'oldstable'} | ${undefined} | ${undefined} | ${'11'} | ${'stable'} + ${'oldstable'} | ${undefined} | ${undefined} | ${'3'} | ${'3'} + ${'oldstable'} | ${'pin'} | ${undefined} | ${'11'} | ${'11'} + ${'oldstable'} | ${'pin'} | ${undefined} | ${'stable'} | ${'11'} + ${'oldstable'} | ${'pin'} | ${undefined} | ${'bullseye'} | ${'11'} + ${'buster'} | ${'pin'} | ${undefined} | ${'11'} | ${'11'} + ${'buster'} | ${'pin'} | ${undefined} | ${'stable'} | ${'11'} + ${'buster'} | ${'pin'} | ${undefined} | ${'bullseye'} | ${'11'} + ${'10'} | ${'pin'} | ${undefined} | ${'11'} | ${'11'} + ${'10'} | ${'pin'} | ${undefined} | ${'stable'} | ${'11'} + ${'10'} | ${'pin'} | ${undefined} | ${'bullseye'} | ${'11'} + `( + 'getNewValue("$currentValue", "$rangeStrategy", "$currentVersion", "$newVersion") === "$expected"', + ({ currentValue, rangeStrategy, currentVersion, newVersion, expected }) => { + spy.mockReturnValue(dt.valueOf()); + expect( + debian.getNewValue({ + currentValue, + rangeStrategy, + currentVersion, + newVersion, + }) + ).toBe(expected); + } + ); + + it.each` + a | b | expected + ${'woody'} | ${'sarge'} | ${-1} + ${'lenny'} | ${'3'} | ${2} + ${'3'} | ${'lenny'} | ${-2} + ${'lenny'} | ${'5'} | ${0} + ${'squeeze'} | ${'6'} | ${0} + ${'10'} | ${'buster'} | ${0} + ${'6'} | ${'squeeze'} | ${0} + ${'buster'} | ${'10'} | ${0} + ${'oldoldstable'} | ${'8'} | ${1} + ${'oldstable'} | ${'oldoldstable'} | ${1} + ${'stable'} | ${'oldstable'} | ${1} + ${'11'} | ${'oldoldstable'} | ${2} + ${'10'} | ${'oldstable'} | ${0} + ${'9'} | ${'stable'} | ${-2} + `('debian.sortVersions($a, $b) === $expected ', ({ a, b, expected }) => { + expect(debian.sortVersions(a, b)).toEqual(expected); + }); + + it.each` + version | range | expected + ${'10'} | ${'10-slim'} | ${false} + ${'11'} | ${'11'} | ${true} + ${'11'} | ${'11.0'} | ${false} + `( + 'matches("$version", "$range") === "$expected"', + ({ version, range, expected }) => { + expect(debian.matches(version, range)).toBe(expected); + } + ); +}); diff --git a/lib/modules/versioning/debian/index.ts b/lib/modules/versioning/debian/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1c9b740ea34068a21f6168b0ee6f173d11bde0a --- /dev/null +++ b/lib/modules/versioning/debian/index.ts @@ -0,0 +1,105 @@ +import type { RangeStrategy } from '../../../types'; +import { DistroInfo } from '../distro'; +import { GenericVersioningApi } from '../generic'; +import type { GenericVersion } from '../generic'; +import type { NewValueConfig, VersioningApi } from '../types'; +import { RollingReleasesData } from './common'; + +export const id = 'debian'; +export const displayName = 'Debian'; +export const urls = [ + 'https://debian.pages.debian.net/distro-info-data/debian.csv', +]; +export const supportsRanges = true; +export const supportedRangeStrategies: RangeStrategy[] = ['pin']; + +const RELEASE_PROP = 'release'; + +export class DebianVersioningApi extends GenericVersioningApi { + private readonly _distroInfo: DistroInfo; + private readonly _rollingReleases: RollingReleasesData; + + constructor() { + super(); + this._distroInfo = new DistroInfo('data/debian-distro-info.json'); + this._rollingReleases = new RollingReleasesData(this._distroInfo); + } + + override isValid(version: string): boolean { + const isValid = super.isValid(version); + const schedule = this._distroInfo.getSchedule( + this._rollingReleases.getVersionByLts(version) + ); + return (isValid && schedule && RELEASE_PROP in schedule) ?? false; + } + + override isStable(version: string): boolean { + let ver: string; + ver = this._rollingReleases.getVersionByLts(version); + ver = this._distroInfo.getVersionByCodename(ver); + return this._distroInfo.isReleased(ver) && !this._distroInfo.isEolLts(ver); + } + + override getNewValue({ + currentValue, + rangeStrategy, + currentVersion, + newVersion, + }: NewValueConfig): string { + if (rangeStrategy === 'pin') { + let newVer = newVersion; + + // convert newVersion to semVer + if (this._distroInfo.isCodename(newVersion)) { + newVer = this._distroInfo.getVersionByCodename(newVersion); + } + if (this._rollingReleases.has(newVersion)) { + newVer = this._rollingReleases.getVersionByLts(newVersion); + } + + // current value is codename or [oldold|old|]stable + if ( + this._distroInfo.isCodename(currentValue) || + this._rollingReleases.has(currentValue) + ) { + return newVer; + } + } + + // current value is [oldold|old|]stable + if (this._rollingReleases.has(currentValue)) { + return this._rollingReleases.getLtsByVersion(newVersion); + } + + if (this._distroInfo.isCodename(currentValue)) { + const di = this._rollingReleases.schedule(newVersion); + let ver = newVersion; + if (di) { + ver = di.version; + } + return this._distroInfo.getCodenameByVersion(ver); + } + + // newVersion is [oldold|old|]stable + // current value is numeric + if (this._rollingReleases.has(newVersion)) { + return this._rollingReleases.schedule(newVersion)?.version ?? newVersion; + } + + return this._distroInfo.getVersionByCodename(newVersion); + } + + protected override _parse(version: string): GenericVersion | null { + let ver: string; + ver = this._rollingReleases.getVersionByLts(version); + ver = this._distroInfo.getVersionByCodename(ver); + if (!this._distroInfo.exists(ver)) { + return null; + } + return { release: ver.split('.').map(Number) }; + } +} + +export const api: VersioningApi = new DebianVersioningApi(); + +export default api; diff --git a/lib/modules/versioning/distro.ts b/lib/modules/versioning/distro.ts index 7d52f6f3aa5586fcfe625213bec598d71c9d2ecd..bdc90f23eab3c41b513582b7f98f0cd35a040937 100644 --- a/lib/modules/versioning/distro.ts +++ b/lib/modules/versioning/distro.ts @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import dataFiles, { DataFile } from '../../data-files.generated'; -interface DistroSchedule { +export interface DistroSchedule { codename: string; series: string; created: string; @@ -13,7 +13,9 @@ interface DistroSchedule { eol_elts?: string; } -export type DistroDataFile = 'data/ubuntu-distro-info.json'; +export type DistroDataFile = + | 'data/ubuntu-distro-info.json' + | 'data/debian-distro-info.json'; export type DistroInfoRecord = Record<string, DistroSchedule>; @@ -48,7 +50,7 @@ export class DistroInfo { for (const v of arr) { const obj = { version: v, ...this._distroInfo[v.toString()] }; - if (!obj.eol) { + if (!obj.release) { // istanbul ignore next continue; } diff --git a/package.json b/package.json index 9ef1a8a55b6bc6023874d9c58edcc91bd1ac9e9b..c8da2617e1054860beb6c84e0863ade4ff490d35 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "test-schema": "run-s create-json-schema", "tsc": "tsc", "type-check": "run-s generate:* \"tsc --noEmit {@}\" --", - "update:ubuntu-distro-info": "node tools/distro-json-generate.mjs", + "update:distro-info": "node tools/distro-json-generate.mjs", "verify": "node tools/verify.mjs" }, "repository": { diff --git a/tools/distro-json-generate.mjs b/tools/distro-json-generate.mjs index 4587351f395c098c9672567783093ee796c0d5fc..4459b27cc8a912305f23d26c853bb77cd7f7c098 100644 --- a/tools/distro-json-generate.mjs +++ b/tools/distro-json-generate.mjs @@ -2,7 +2,8 @@ import fs from 'fs-extra'; import got from 'got'; import shell from 'shelljs'; -const url = 'https://debian.pages.debian.net/distro-info-data/ubuntu.csv'; +const ubuntuUrl = 'https://debian.pages.debian.net/distro-info-data/ubuntu.csv'; +const debianUrl = 'https://debian.pages.debian.net/distro-info-data/debian.csv'; /** * Converts valid CSV string into JSON. @@ -73,5 +74,7 @@ async function update(url, file) { await updateJsonFile(file, json); } -// eslint-disable-next-line @typescript-eslint/no-floating-promises -update(url, `./data/ubuntu-distro-info.json`); +(async () => { + await update(ubuntuUrl, `./data/ubuntu-distro-info.json`); + await update(debianUrl, `./data/debian-distro-info.json`); +})().catch(() => 'obligatory catch');