From 06264fd206c8fe16dcf16c14bc23464da8f044a4 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Fri, 11 Oct 2024 01:54:28 -0300
Subject: [PATCH] feat(sbt-package): Support `releaseTimestamp` (#31882)

---
 .../datasource/sbt-package/index.spec.ts      | 36 ++++++++++
 lib/modules/datasource/sbt-package/index.ts   | 70 +++++++++++++++----
 2 files changed, 93 insertions(+), 13 deletions(-)

diff --git a/lib/modules/datasource/sbt-package/index.spec.ts b/lib/modules/datasource/sbt-package/index.spec.ts
index 85e6e33d29..45510d2c94 100644
--- a/lib/modules/datasource/sbt-package/index.spec.ts
+++ b/lib/modules/datasource/sbt-package/index.spec.ts
@@ -2,12 +2,17 @@ import { codeBlock } from 'common-tags';
 import { getPkgReleases } from '..';
 import { Fixtures } from '../../../../test/fixtures';
 import * as httpMock from '../../../../test/http-mock';
+import { mocked } from '../../../../test/util';
+import * as _packageCache from '../../../util/cache/package';
 import { regEx } from '../../../util/regex';
 import * as mavenVersioning from '../../versioning/maven';
 import { MAVEN_REPO } from '../maven/common';
 import { extractPageLinks } from './util';
 import { SbtPackageDatasource } from '.';
 
+jest.mock('../../../util/cache/package');
+const packageCache = mocked(_packageCache);
+
 describe('modules/datasource/sbt-package/index', () => {
   it('parses Maven index directory', () => {
     expect(
@@ -278,4 +283,35 @@ describe('modules/datasource/sbt-package/index', () => {
       expect(res).toMatchObject({});
     });
   });
+
+  describe('postprocessRelease', () => {
+    const datasource = new SbtPackageDatasource();
+
+    it('extracts URL from Maven POM file', async () => {
+      const registryUrl = 'https://repo.maven.apache.org/maven2/';
+      const packageName = 'org.example:example';
+      packageCache.get.mockImplementation(((ns: string, k: string) =>
+        ns === 'datasource-sbt-package' &&
+        k === `package-urls:${registryUrl}:${packageName}`
+          ? Promise.resolve([`${registryUrl}org/example/`])
+          : Promise.resolve(undefined)) as never);
+
+      httpMock
+        .scope(registryUrl)
+        .get('/org/example/1.2.3/example-1.2.3.pom')
+        .reply(200, codeBlock`<project></project>`, {
+          'last-modified': 'Wed, 21 Oct 2015 07:28:00 GMT',
+        });
+
+      const res = await datasource.postprocessRelease(
+        { packageName, registryUrl },
+        { version: '1.2.3' },
+      );
+
+      expect(res).toEqual({
+        version: '1.2.3',
+        releaseTimestamp: '2015-10-21T07:28:00.000Z',
+      });
+    });
+  });
 });
diff --git a/lib/modules/datasource/sbt-package/index.ts b/lib/modules/datasource/sbt-package/index.ts
index 519ed4097f..463661b51c 100644
--- a/lib/modules/datasource/sbt-package/index.ts
+++ b/lib/modules/datasource/sbt-package/index.ts
@@ -2,6 +2,7 @@ import * as upath from 'upath';
 import { XmlDocument } from 'xmldoc';
 import { logger } from '../../../logger';
 import * as packageCache from '../../../util/cache/package';
+import { cache } from '../../../util/cache/package/decorator';
 import { Http } from '../../../util/http';
 import { regEx } from '../../../util/regex';
 import { ensureTrailingSlash, trimTrailingSlash } from '../../../util/url';
@@ -10,6 +11,7 @@ import { compare } from '../../versioning/maven/compare';
 import { MavenDatasource } from '../maven';
 import { MAVEN_REPO } from '../maven/common';
 import { downloadHttpProtocol } from '../maven/util';
+import { normalizeDate } from '../metadata';
 import type {
   GetReleasesConfig,
   PostprocessReleaseConfig,
@@ -26,6 +28,12 @@ interface ScalaDepCoordinate {
   scalaVersion?: string;
 }
 
+interface PomInfo {
+  homepage?: string;
+  sourceUrl?: string;
+  releaseTimestamp?: string;
+}
+
 export class SbtPackageDatasource extends MavenDatasource {
   static override readonly id = 'sbt-package';
 
@@ -187,6 +195,11 @@ export class SbtPackageDatasource extends MavenDatasource {
       return null;
     }
 
+    const releases: Release[] = [...allVersions]
+      .sort(compare)
+      .map((version) => ({ version }));
+    const res: ReleaseResult = { releases, dependencyUrl };
+
     const latestVersion = getLatestVersion(versions);
     const pomInfo = await this.getPomInfo(
       registryUrl,
@@ -195,10 +208,15 @@ export class SbtPackageDatasource extends MavenDatasource {
       packageUrls,
     );
 
-    const releases: Release[] = [...allVersions]
-      .sort(compare)
-      .map((version) => ({ version }));
-    return { releases, dependencyUrl, ...pomInfo };
+    if (pomInfo?.homepage) {
+      res.homepage = pomInfo.homepage;
+    }
+
+    if (pomInfo?.sourceUrl) {
+      res.sourceUrl = pomInfo.sourceUrl;
+    }
+
+    return res;
   }
 
   async getPomInfo(
@@ -206,9 +224,7 @@ export class SbtPackageDatasource extends MavenDatasource {
     packageName: string,
     version: string | null,
     pkgUrls?: string[],
-  ): Promise<Pick<ReleaseResult, 'homepage' | 'sourceUrl'>> {
-    const result: Pick<ReleaseResult, 'homepage' | 'sourceUrl'> = {};
-
+  ): Promise<PomInfo | null> {
     const packageUrlsKey = `package-urls:${registryUrl}:${packageName}`;
     // istanbul ignore next: will be covered later
     const packageUrls =
@@ -220,12 +236,12 @@ export class SbtPackageDatasource extends MavenDatasource {
 
     // istanbul ignore if
     if (!packageUrls?.length) {
-      return result;
+      return null;
     }
 
     // istanbul ignore if
     if (!version) {
-      return result;
+      return null;
     }
 
     const invalidPomFilesKey = `invalid-pom-files:${registryUrl}:${packageName}:${version}`;
@@ -265,6 +281,13 @@ export class SbtPackageDatasource extends MavenDatasource {
           continue;
         }
 
+        const result: PomInfo = {};
+
+        const releaseTimestamp = normalizeDate(res.headers['last-modified']);
+        if (releaseTimestamp) {
+          result.releaseTimestamp = releaseTimestamp;
+        }
+
         const pomXml = new XmlDocument(content);
 
         const homepage = pomXml.valueWithPath('url');
@@ -287,7 +310,7 @@ export class SbtPackageDatasource extends MavenDatasource {
     }
 
     await saveCache();
-    return result;
+    return null;
   }
 
   override async getReleases(
@@ -316,12 +339,33 @@ export class SbtPackageDatasource extends MavenDatasource {
     return null;
   }
 
-  // istanbul ignore next: to be rewritten
+  @cache({
+    namespace: 'datasource-sbt-package',
+    key: (
+      { registryUrl, packageName }: PostprocessReleaseConfig,
+      { version }: Release,
+    ) => `postprocessRelease:${registryUrl}:${packageName}:${version}`,
+    ttlMinutes: 30 * 24 * 60,
+  })
   override async postprocessRelease(
     config: PostprocessReleaseConfig,
     release: Release,
   ): Promise<PostprocessReleaseResult> {
-    const mavenResult = await super.postprocessRelease(config, release);
-    return mavenResult === 'reject' ? release : mavenResult;
+    // istanbul ignore if
+    if (!config.registryUrl) {
+      return release;
+    }
+
+    const res = await this.getPomInfo(
+      config.registryUrl,
+      config.packageName,
+      release.version,
+    );
+
+    if (res?.releaseTimestamp) {
+      release.releaseTimestamp = res.releaseTimestamp;
+    }
+
+    return release;
   }
 }
-- 
GitLab