From 2b2d306fb0f9ba7c7f75b31b62293c5667ff9b51 Mon Sep 17 00:00:00 2001
From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com>
Date: Thu, 16 Jan 2025 08:43:46 +0000
Subject: [PATCH] fix(versioning/ubuntu): support suffixed codename versions
 (#33308)

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 lib/modules/versioning/ubuntu/common.ts     |  18 +-
 lib/modules/versioning/ubuntu/index.spec.ts | 248 ++++++++++----------
 lib/modules/versioning/ubuntu/index.ts      |  16 ++
 3 files changed, 161 insertions(+), 121 deletions(-)

diff --git a/lib/modules/versioning/ubuntu/common.ts b/lib/modules/versioning/ubuntu/common.ts
index abcb173c04..0cd4b958c7 100644
--- a/lib/modules/versioning/ubuntu/common.ts
+++ b/lib/modules/versioning/ubuntu/common.ts
@@ -1,11 +1,13 @@
 import { regEx } from '../../../util/regex';
 
+const regex = regEx(/^(?<codename>\w+)-(?<date>\d{8})(?<suffix>\.\d{1,2})?$/);
+
 function isDatedCodeName(input: string): boolean {
-  return regEx(/^(?<codename>\w+)-(?<date>\d{8})$/).test(input);
+  return regex.test(input);
 }
 
 function getDatedContainerImageCodename(version: string): null | string {
-  const groups = regEx(/^(?<codename>\w+)-(?<date>\d{8})$/).exec(version);
+  const groups = regex.exec(version);
   if (!groups?.groups) {
     return null;
   }
@@ -13,7 +15,7 @@ function getDatedContainerImageCodename(version: string): null | string {
 }
 
 function getDatedContainerImageVersion(version: string): null | number {
-  const groups = regEx(/^(?<codename>\w+)-(?<date>\d{8})$/).exec(version);
+  const groups = regex.exec(version);
   if (!groups?.groups) {
     return null;
   }
@@ -21,8 +23,18 @@ function getDatedContainerImageVersion(version: string): null | number {
   return parseInt(groups.groups.date, 10);
 }
 
+function getDatedContainerImageSuffix(version: string): null | string {
+  const groups = regex.exec(version);
+  if (!groups?.groups?.suffix) {
+    return null;
+  }
+
+  return groups.groups.suffix;
+}
+
 export {
   isDatedCodeName,
   getDatedContainerImageCodename,
   getDatedContainerImageVersion,
+  getDatedContainerImageSuffix,
 };
diff --git a/lib/modules/versioning/ubuntu/index.spec.ts b/lib/modules/versioning/ubuntu/index.spec.ts
index eaa5de4e28..5a60006fa9 100644
--- a/lib/modules/versioning/ubuntu/index.spec.ts
+++ b/lib/modules/versioning/ubuntu/index.spec.ts
@@ -5,84 +5,88 @@ describe('modules/versioning/ubuntu/index', () => {
   const dt = DateTime.fromISO('2022-04-20');
 
   it.each`
-    version             | expected
-    ${undefined}        | ${false}
-    ${null}             | ${false}
-    ${''}               | ${false}
-    ${'xenial'}         | ${true}
-    ${'04.10'}          | ${true}
-    ${'05.04'}          | ${true}
-    ${'05.10'}          | ${true}
-    ${'6.06'}           | ${true}
-    ${'6.10'}           | ${true}
-    ${'7.04'}           | ${true}
-    ${'7.10'}           | ${true}
-    ${'8.04'}           | ${true}
-    ${'8.10'}           | ${true}
-    ${'9.04'}           | ${true}
-    ${'9.10'}           | ${true}
-    ${'10.04.4'}        | ${true}
-    ${'10.10'}          | ${true}
-    ${'11.04'}          | ${true}
-    ${'11.10'}          | ${true}
-    ${'12.04.5'}        | ${true}
-    ${'12.10'}          | ${true}
-    ${'13.04'}          | ${true}
-    ${'13.10'}          | ${true}
-    ${'14.04.6'}        | ${true}
-    ${'14.10'}          | ${true}
-    ${'15.04'}          | ${true}
-    ${'15.10'}          | ${true}
-    ${'16.04.7'}        | ${true}
-    ${'16.10'}          | ${true}
-    ${'17.04'}          | ${true}
-    ${'17.10'}          | ${true}
-    ${'18.04.5'}        | ${true}
-    ${'18.10'}          | ${true}
-    ${'19.04'}          | ${true}
-    ${'19.10'}          | ${true}
-    ${'20.04'}          | ${true}
-    ${'20.10'}          | ${true}
-    ${'2020.04'}        | ${false}
-    ${'xenial'}         | ${true}
-    ${'warty'}          | ${true}
-    ${'hoary'}          | ${true}
-    ${'breezy'}         | ${true}
-    ${'dapper'}         | ${true}
-    ${'edgy'}           | ${true}
-    ${'feisty'}         | ${true}
-    ${'gutsy'}          | ${true}
-    ${'hardy'}          | ${true}
-    ${'intrepid'}       | ${true}
-    ${'jaunty'}         | ${true}
-    ${'karmic'}         | ${true}
-    ${'lucid.4'}        | ${false}
-    ${'maverick'}       | ${true}
-    ${'natty'}          | ${true}
-    ${'oneiric'}        | ${true}
-    ${'precise.5'}      | ${false}
-    ${'quantal'}        | ${true}
-    ${'raring'}         | ${true}
-    ${'saucy'}          | ${true}
-    ${'trusty.6'}       | ${false}
-    ${'utopic'}         | ${true}
-    ${'vivid'}          | ${true}
-    ${'wily'}           | ${true}
-    ${'xenial.7'}       | ${false}
-    ${'yakkety'}        | ${true}
-    ${'zesty'}          | ${true}
-    ${'artful'}         | ${true}
-    ${'bionic.5'}       | ${false}
-    ${'cosmic'}         | ${true}
-    ${'disco'}          | ${true}
-    ${'eoan'}           | ${true}
-    ${'focal'}          | ${true}
-    ${'groovy'}         | ${true}
-    ${'hirsute'}        | ${true}
-    ${'impish'}         | ${true}
-    ${'jammy'}          | ${true}
-    ${'jammy-20230816'} | ${true}
-    ${'jammy-2023086'}  | ${false}
+    version                   | expected
+    ${undefined}              | ${false}
+    ${null}                   | ${false}
+    ${''}                     | ${false}
+    ${'xenial'}               | ${true}
+    ${'04.10'}                | ${true}
+    ${'05.04'}                | ${true}
+    ${'05.10'}                | ${true}
+    ${'6.06'}                 | ${true}
+    ${'6.10'}                 | ${true}
+    ${'7.04'}                 | ${true}
+    ${'7.10'}                 | ${true}
+    ${'8.04'}                 | ${true}
+    ${'8.10'}                 | ${true}
+    ${'9.04'}                 | ${true}
+    ${'9.10'}                 | ${true}
+    ${'10.04.4'}              | ${true}
+    ${'10.10'}                | ${true}
+    ${'11.04'}                | ${true}
+    ${'11.10'}                | ${true}
+    ${'12.04.5'}              | ${true}
+    ${'12.10'}                | ${true}
+    ${'13.04'}                | ${true}
+    ${'13.10'}                | ${true}
+    ${'14.04.6'}              | ${true}
+    ${'14.10'}                | ${true}
+    ${'15.04'}                | ${true}
+    ${'15.10'}                | ${true}
+    ${'16.04.7'}              | ${true}
+    ${'16.10'}                | ${true}
+    ${'17.04'}                | ${true}
+    ${'17.10'}                | ${true}
+    ${'18.04.5'}              | ${true}
+    ${'18.10'}                | ${true}
+    ${'19.04'}                | ${true}
+    ${'19.10'}                | ${true}
+    ${'20.04'}                | ${true}
+    ${'20.10'}                | ${true}
+    ${'2020.04'}              | ${false}
+    ${'xenial'}               | ${true}
+    ${'warty'}                | ${true}
+    ${'hoary'}                | ${true}
+    ${'breezy'}               | ${true}
+    ${'dapper'}               | ${true}
+    ${'edgy'}                 | ${true}
+    ${'feisty'}               | ${true}
+    ${'gutsy'}                | ${true}
+    ${'hardy'}                | ${true}
+    ${'intrepid'}             | ${true}
+    ${'jaunty'}               | ${true}
+    ${'karmic'}               | ${true}
+    ${'lucid.4'}              | ${false}
+    ${'maverick'}             | ${true}
+    ${'natty'}                | ${true}
+    ${'oneiric'}              | ${true}
+    ${'precise.5'}            | ${false}
+    ${'quantal'}              | ${true}
+    ${'raring'}               | ${true}
+    ${'saucy'}                | ${true}
+    ${'trusty.6'}             | ${false}
+    ${'utopic'}               | ${true}
+    ${'vivid'}                | ${true}
+    ${'wily'}                 | ${true}
+    ${'xenial.7'}             | ${false}
+    ${'yakkety'}              | ${true}
+    ${'zesty'}                | ${true}
+    ${'artful'}               | ${true}
+    ${'bionic.5'}             | ${false}
+    ${'cosmic'}               | ${true}
+    ${'disco'}                | ${true}
+    ${'eoan'}                 | ${true}
+    ${'focal'}                | ${true}
+    ${'groovy'}               | ${true}
+    ${'hirsute'}              | ${true}
+    ${'impish'}               | ${true}
+    ${'jammy'}                | ${true}
+    ${'jammy-20230816'}       | ${true}
+    ${'jammy-20230816'}       | ${true}
+    ${'yakkety-20160806.1'}   | ${true}
+    ${'utopic-20150228.11'}   | ${true}
+    ${'utopic-20150228.11.1'} | ${false}
+    ${'oracular-20240811.'}   | ${false}
   `('isValid("$version") === $expected', ({ version, expected }) => {
     expect(ubuntu.isValid(version)).toBe(expected);
   });
@@ -272,51 +276,59 @@ describe('modules/versioning/ubuntu/index', () => {
   );
 
   it.each`
-    a                   | b                   | expected
-    ${'20.04'}          | ${'2020.04'}        | ${false}
-    ${'17.10'}          | ${'artful'}         | ${true}
-    ${'xenial'}         | ${'artful'}         | ${false}
-    ${'17.04'}          | ${'artful'}         | ${false}
-    ${'artful'}         | ${'17.10'}          | ${true}
-    ${'16.04'}          | ${'xenial'}         | ${true}
-    ${'focal'}          | ${'20.04'}          | ${true}
-    ${'20.04'}          | ${'focal'}          | ${true}
-    ${'19.10'}          | ${'19.10'}          | ${true}
-    ${'jammy'}          | ${'jammy-20230816'} | ${false}
-    ${'jammy-20230816'} | ${'jammy-20230816'} | ${true}
-    ${'jammy-20230716'} | ${'jammy-20230816'} | ${false}
+    a                     | b                      | expected
+    ${'20.04'}            | ${'2020.04'}           | ${false}
+    ${'17.10'}            | ${'artful'}            | ${true}
+    ${'xenial'}           | ${'artful'}            | ${false}
+    ${'17.04'}            | ${'artful'}            | ${false}
+    ${'artful'}           | ${'17.10'}             | ${true}
+    ${'16.04'}            | ${'xenial'}            | ${true}
+    ${'focal'}            | ${'20.04'}             | ${true}
+    ${'20.04'}            | ${'focal'}             | ${true}
+    ${'19.10'}            | ${'19.10'}             | ${true}
+    ${'jammy'}            | ${'jammy-20230816'}    | ${false}
+    ${'jammy-20230816'}   | ${'jammy-20230816'}    | ${true}
+    ${'jammy-20230716'}   | ${'jammy-20230816'}    | ${false}
+    ${'jammy-20230716.1'} | ${'jammy-20230716.1'}  | ${true}
+    ${'jammy-20230716.1'} | ${'jammy-20230716.2'}  | ${false}
+    ${'jammy-20230716.1'} | ${'jammy-20230816.11'} | ${false}
   `('equals($a, $b) === $expected', ({ a, b, expected }) => {
     expect(ubuntu.equals(a, b)).toBe(expected);
   });
 
   it.each`
-    a                   | b                   | expected
-    ${'20.04'}          | ${'20.10'}          | ${false}
-    ${'20.10'}          | ${'20.04'}          | ${true}
-    ${'19.10'}          | ${'20.04'}          | ${false}
-    ${'20.04'}          | ${'19.10'}          | ${true}
-    ${'16.04'}          | ${'16.04.7'}        | ${false}
-    ${'16.04.7'}        | ${'16.04'}          | ${true}
-    ${'16.04.1'}        | ${'16.04.7'}        | ${false}
-    ${'16.04.7'}        | ${'16.04.1'}        | ${true}
-    ${'19.10.1'}        | ${'20.04.1'}        | ${false}
-    ${'20.04.1'}        | ${'19.10.1'}        | ${true}
-    ${'xxx'}            | ${'yyy'}            | ${false}
-    ${'focal'}          | ${'groovy'}         | ${false}
-    ${'groovy'}         | ${'focal'}          | ${true}
-    ${'eoan'}           | ${'focal'}          | ${false}
-    ${'focal'}          | ${'eoan'}           | ${true}
-    ${'vivid'}          | ${'saucy'}          | ${true}
-    ${'impish'}         | ${'focal'}          | ${true}
-    ${'eoan'}           | ${'quantal'}        | ${true}
-    ${'focal'}          | ${'lucid'}          | ${true}
-    ${'eoan'}           | ${'focal'}          | ${false}
-    ${'focal'}          | ${'eoan'}           | ${true}
-    ${'jammy'}          | ${'focal'}          | ${true}
-    ${'jammy-20230816'} | ${'focal'}          | ${true}
-    ${'jammy-20230816'} | ${'jammy-20230716'} | ${true}
-    ${'jammy-20230716'} | ${'jammy-20230816'} | ${false}
-    ${'focal-20230816'} | ${'jammy-20230716'} | ${false}
+    a                     | b                        | expected
+    ${'20.04'}            | ${'20.10'}               | ${false}
+    ${'20.10'}            | ${'20.04'}               | ${true}
+    ${'19.10'}            | ${'20.04'}               | ${false}
+    ${'20.04'}            | ${'19.10'}               | ${true}
+    ${'16.04'}            | ${'16.04.7'}             | ${false}
+    ${'16.04.7'}          | ${'16.04'}               | ${true}
+    ${'16.04.1'}          | ${'16.04.7'}             | ${false}
+    ${'16.04.7'}          | ${'16.04.1'}             | ${true}
+    ${'19.10.1'}          | ${'20.04.1'}             | ${false}
+    ${'20.04.1'}          | ${'19.10.1'}             | ${true}
+    ${'xxx'}              | ${'yyy'}                 | ${false}
+    ${'focal'}            | ${'groovy'}              | ${false}
+    ${'groovy'}           | ${'focal'}               | ${true}
+    ${'eoan'}             | ${'focal'}               | ${false}
+    ${'focal'}            | ${'eoan'}                | ${true}
+    ${'vivid'}            | ${'saucy'}               | ${true}
+    ${'impish'}           | ${'focal'}               | ${true}
+    ${'eoan'}             | ${'quantal'}             | ${true}
+    ${'focal'}            | ${'lucid'}               | ${true}
+    ${'eoan'}             | ${'focal'}               | ${false}
+    ${'focal'}            | ${'eoan'}                | ${true}
+    ${'jammy'}            | ${'focal'}               | ${true}
+    ${'jammy-20230816'}   | ${'focal'}               | ${true}
+    ${'jammy-20230816'}   | ${'jammy-20230716'}      | ${true}
+    ${'jammy-20230716'}   | ${'jammy-20230816'}      | ${false}
+    ${'focal-20230816'}   | ${'jammy-20230716'}      | ${false}
+    ${'zesty-20170517.1'} | ${'jammy-20240627.1'}    | ${false}
+    ${'jammy-20240627.3'} | ${'jammy-20240627.1'}    | ${true}
+    ${'jammy-20240627.3'} | ${'jammy-20240627.4'}    | ${false}
+    ${'jammy-20240627.1'} | ${'precise-20150228.11'} | ${true}
+    ${'jammy-20240627'}   | ${'precise-20150228.11'} | ${true}
   `('isGreaterThan("$a", "$b") === $expected', ({ a, b, expected }) => {
     expect(ubuntu.isGreaterThan(a, b)).toBe(expected);
   });
diff --git a/lib/modules/versioning/ubuntu/index.ts b/lib/modules/versioning/ubuntu/index.ts
index 338b419ecb..ce41ab1a3a 100644
--- a/lib/modules/versioning/ubuntu/index.ts
+++ b/lib/modules/versioning/ubuntu/index.ts
@@ -4,6 +4,7 @@ import { DistroInfo } from '../distro';
 import type { NewValueConfig, VersioningApi } from '../types';
 import {
   getDatedContainerImageCodename,
+  getDatedContainerImageSuffix,
   getDatedContainerImageVersion,
   isDatedCodeName,
 } from './common';
@@ -105,6 +106,12 @@ function equals(version: string, other: string): boolean {
     return false;
   }
 
+  const verSuffix = getDatedContainerImageSuffix(version);
+  const otherSuffix = getDatedContainerImageSuffix(other);
+  if (verSuffix !== otherSuffix) {
+    return false;
+  }
+
   const ver = getVersionByCodename(version);
   const otherVer = getVersionByCodename(other);
   return isVersion(ver) && isVersion(otherVer) && ver === otherVer;
@@ -138,6 +145,15 @@ function isGreaterThan(version: string, other: string): boolean {
     return false;
   }
 
+  const xSuffixVersion = getDatedContainerImageSuffix(version) ?? 0;
+  const ySuffixVersion = getDatedContainerImageSuffix(other) ?? 0;
+  if (xSuffixVersion > ySuffixVersion) {
+    return true;
+  }
+  if (xSuffixVersion < ySuffixVersion) {
+    return false;
+  }
+
   const xPatch = getPatch(version) ?? 0;
   const yPatch = getPatch(other) ?? 0;
   return xPatch > yPatch;
-- 
GitLab