diff --git a/lib/datasource/sbt-package/index.spec.ts b/lib/datasource/sbt-package/index.spec.ts index 1efe92dd0d0762806de5a759ea23e1e272a6d5a5..5bd0da728a8cc870162f8ced0265c6b148f6fe83 100644 --- a/lib/datasource/sbt-package/index.spec.ts +++ b/lib/datasource/sbt-package/index.spec.ts @@ -29,14 +29,24 @@ describe('datasource/sbt', () => { beforeEach(() => { nock.disableNetConnect(); nock('https://failed_repo').get('/maven/org/scalatest/').reply(404, null); + nock('https://repo.maven.apache.org') + .get('/maven2/com/example/') + .reply(200, '<a href="empty/">empty_2.12/</a>\n'); + nock('https://repo.maven.apache.org') + .get('/maven2/com/example/empty/') + .reply(200, ''); nock('https://repo.maven.apache.org') .get('/maven2/org/scalatest/') + .times(3) .reply( 200, '<a href="scalatest/" title=\'scalatest/\'>scalatest_2.12/</a>\n' + '<a href="scalatest_2.12/" title=\'scalatest_2.12/\'>scalatest_2.12/</a>\n' + "<a href='scalatest_sjs2.12/'>scalatest_2.12/</a>" + - "<a href='scalatest_native2.12/'>scalatest_2.12/</a>" + "<a href='scalatest_native2.12/'>scalatest_2.12/</a>" + + '<a href="scalatest-app_2.12/">scalatest-app_2.12</a>' + + '<a href="scalatest-flatspec_2.12/">scalatest-flatspec_2.12</a>' + + '<a href="scalatest-matchers-core_2.12/">scalatest-matchers-core_2.12</a>' ); nock('https://repo.maven.apache.org') .get('/maven2/org/scalatest/scalatest/') @@ -44,6 +54,50 @@ describe('datasource/sbt', () => { nock('https://repo.maven.apache.org') .get('/maven2/org/scalatest/scalatest_2.12/') .reply(200, "<a href='1.2.3/'>4.5.6/</a>"); + nock('https://repo.maven.apache.org') + .get('/maven2/org/scalatest/scalatest-app_2.12/') + .reply(200, "<a href='6.5.4/'>3.2.1/</a>"); + nock('https://repo.maven.apache.org') + .get('/maven2/org/scalatest/scalatest-flatspec_2.12/') + .reply(200, "<a href='6.5.4/'>3.2.1/</a>"); + nock('https://repo.maven.apache.org') + .get('/maven2/org/scalatest/scalatest-matchers-core_2.12/') + .reply(200, "<a href='6.5.4/'>3.2.1/</a>"); + nock('https://repo.maven.apache.org') + .get( + '/maven2/org/scalatest/scalatest-app_2.12/6.5.4/scalatest-app_2.12-6.5.4.pom' + ) + .reply( + 200, + '<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">' + + '<url>http://www.scalatest.org</url>' + + '<scm>' + + '<url>https://github.com/scalatest/scalatest</url>' + + '</scm>' + + '</project>' + ); + nock('https://repo.maven.apache.org') + .get( + '/maven2/org/scalatest/scalatest-flatspec_2.12/6.5.4/scalatest-flatspec_2.12-6.5.4.pom' + ) + .reply( + 200, + '<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">' + + '<scm>' + + '<url>scm:git:git:git@github.com/scalatest/scalatest</url>' + + '</scm>' + + '</project>' + ); + nock('https://repo.maven.apache.org') + .get( + '/maven2/org/scalatest/scalatest-matchers-core_2.12/6.5.4/scalatest-matchers-core_2.12-6.5.4.pom' + ) + .reply( + 200, + '<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">' + + '<url>http://www.scalatest.org</url>' + + '</project>' + ); nock('https://dl.bintray.com') .get('/sbt/sbt-plugin-releases/com.github.gseitz/') @@ -106,6 +160,16 @@ describe('datasource/sbt', () => { }) ).toBeNull(); }); + it('returns null if there is no version', async () => { + expect( + await getPkgReleases({ + versioning: mavenVersioning.id, + datasource: sbtPlugin.id, + depName: 'com.example:empty', + registryUrls: [], + }) + ).toBeNull(); + }); it('fetches releases from Maven', async () => { expect( await getPkgReleases({ @@ -136,5 +200,54 @@ describe('datasource/sbt', () => { releases: [{ version: '1.2.3' }], }); }); + + it('extracts URL from Maven POM file', async () => { + expect( + await getPkgReleases({ + versioning: mavenVersioning.id, + datasource: sbtPlugin.id, + depName: 'org.scalatest:scalatest-app_2.12', + registryUrls: [], + }) + ).toEqual({ + dependencyUrl: 'https://repo.maven.apache.org/maven2/org/scalatest', + display: 'org.scalatest:scalatest-app_2.12', + group: 'org.scalatest', + name: 'scalatest-app_2.12', + releases: [{ version: '6.5.4' }], + homepage: 'http://www.scalatest.org', + sourceUrl: 'https://github.com/scalatest/scalatest', + }); + expect( + await getPkgReleases({ + versioning: mavenVersioning.id, + datasource: sbtPlugin.id, + depName: 'org.scalatest:scalatest-flatspec_2.12', + registryUrls: [], + }) + ).toEqual({ + dependencyUrl: 'https://repo.maven.apache.org/maven2/org/scalatest', + display: 'org.scalatest:scalatest-flatspec_2.12', + group: 'org.scalatest', + name: 'scalatest-flatspec_2.12', + releases: [{ version: '6.5.4' }], + sourceUrl: 'https://github.com/scalatest/scalatest', + }); + expect( + await getPkgReleases({ + versioning: mavenVersioning.id, + datasource: sbtPlugin.id, + depName: 'org.scalatest:scalatest-matchers-core_2.12', + registryUrls: [], + }) + ).toEqual({ + dependencyUrl: 'https://repo.maven.apache.org/maven2/org/scalatest', + display: 'org.scalatest:scalatest-matchers-core_2.12', + group: 'org.scalatest', + name: 'scalatest-matchers-core_2.12', + releases: [{ version: '6.5.4' }], + homepage: 'http://www.scalatest.org', + }); + }); }); }); diff --git a/lib/datasource/sbt-package/index.ts b/lib/datasource/sbt-package/index.ts index e5ea80a6d9359d133cb1b1a4fa946342a4438a6e..37f0144404afdc70ac137b1d330c5ec33eaa9f6c 100644 --- a/lib/datasource/sbt-package/index.ts +++ b/lib/datasource/sbt-package/index.ts @@ -1,3 +1,4 @@ +import { XmlDocument } from 'xmldoc'; import { logger } from '../../logger'; import { compare } from '../../versioning/maven/compare'; import { GetReleasesConfig, ReleaseResult } from '../common'; @@ -12,7 +13,7 @@ export const registryStrategy = 'hunt'; const ensureTrailingSlash = (str: string): string => str.replace(/\/?$/, '/'); -export async function resolvePackageReleases( +export async function getArtifactSubdirs( searchRoot: string, artifact: string, scalaVersion: string @@ -22,7 +23,6 @@ export async function resolvePackageReleases( 'sbt' ); if (indexContent) { - const releases: string[] = []; const parseSubdirs = (content: string): string[] => parseIndexDir(content, (x) => { if (x === artifact) { @@ -36,17 +36,28 @@ export async function resolvePackageReleases( } return x.startsWith(`${artifact}_`); }); - const artifactSubdirs = parseSubdirs(indexContent); - let searchSubdirs = artifactSubdirs; + let artifactSubdirs = parseSubdirs(indexContent); if ( scalaVersion && artifactSubdirs.includes(`${artifact}_${scalaVersion}`) ) { - searchSubdirs = [`${artifact}_${scalaVersion}`]; + artifactSubdirs = [`${artifact}_${scalaVersion}`]; } + return artifactSubdirs; + } + + return null; +} + +export async function getPackageReleases( + searchRoot: string, + artifactSubdirs: string[] +): Promise<string[]> { + if (artifactSubdirs) { + const releases: string[] = []; const parseReleases = (content: string): string[] => parseIndexDir(content, (x) => !/^\.+$/.test(x)); - for (const searchSubdir of searchSubdirs) { + for (const searchSubdir of artifactSubdirs) { const content = await downloadHttpProtocol( ensureTrailingSlash(`${searchRoot}/${searchSubdir}`), 'sbt' @@ -64,6 +75,66 @@ export async function resolvePackageReleases( return null; } +export function getLatestVersion(versions: string[]): string | null { + if (versions?.length) { + return versions.reduce((latestVersion, version) => + compare(version, latestVersion) === 1 ? version : latestVersion + ); + } + return null; +} + +export async function getUrls( + searchRoot: string, + artifactDirs: string[], + version: string +): Promise<Partial<ReleaseResult>> { + const result: Partial<ReleaseResult> = {}; + + if (!artifactDirs?.length) { + return result; + } + + if (!version) { + return result; + } + + for (const artifactDir of artifactDirs) { + const [artifact] = artifactDir.split('_'); + const pomFileNames = [ + `${artifactDir}-${version}.pom`, + `${artifact}-${version}.pom`, + ]; + + for (const pomFileName of pomFileNames) { + const pomUrl = `${searchRoot}/${artifactDir}/${version}/${pomFileName}`; + const content = await downloadHttpProtocol(pomUrl, 'sbt'); + + if (content) { + const pomXml = new XmlDocument(content); + + const homepage = pomXml.valueWithPath('url'); + if (homepage) { + result.homepage = homepage; + } + + const sourceUrl = pomXml.valueWithPath('scm.url'); + if (sourceUrl) { + result.sourceUrl = sourceUrl + .replace(/^scm:/, '') + .replace(/^git:/, '') + .replace(/^git@github.com:/, 'https://github.com/') + .replace(/\.git$/, ''); + } + + return result; + } + } + } + + return result; +} + export async function getReleases({ lookupName, registryUrl, @@ -81,16 +152,20 @@ export async function getReleases({ for (let idx = 0; idx < searchRoots.length; idx += 1) { const searchRoot = searchRoots[idx]; - const versions = await resolvePackageReleases( + const artifactSubdirs = await getArtifactSubdirs( searchRoot, artifact, scalaVersion ); + const versions = await getPackageReleases(searchRoot, artifactSubdirs); + const latestVersion = getLatestVersion(versions); + const urls = await getUrls(searchRoot, artifactSubdirs, latestVersion); const dependencyUrl = searchRoot; if (versions) { return { + ...urls, display: lookupName, group: groupId, name: artifactId, diff --git a/lib/datasource/sbt-plugin/index.spec.ts b/lib/datasource/sbt-plugin/index.spec.ts index ee59fe64ea42408a3d5bdb2084f531cc3e7f226a..e7eeadd9da2bc8aaaae0cf910e91ea84ed482f5b 100644 --- a/lib/datasource/sbt-plugin/index.spec.ts +++ b/lib/datasource/sbt-plugin/index.spec.ts @@ -3,6 +3,7 @@ import path from 'path'; import nock from 'nock'; import { getPkgReleases } from '..'; import * as mavenVersioning from '../../versioning/maven'; +import { MAVEN_REPO } from '../maven/common'; import { parseIndexDir } from './util'; import * as sbtPlugin from '.'; @@ -89,6 +90,38 @@ describe('datasource/sbt', () => { '</body>\n' + '</html>\n' ); + + nock('https://repo.maven.apache.org') + .get('/maven2/io/get-coursier/') + .reply( + 200, + '<a href="sbt-coursier_2.10_0.13/">sbt-coursier_2.10_0.13/</a>\n' + + '<a href="sbt-coursier_2.12_1.0/">sbt-coursier_2.12_1.0/</a>\n' + + '<a href="sbt-coursier_2.12_1.0.0-M5/">sbt-coursier_2.12_1.0.0-M5/</a>\n' + + '<a href="sbt-coursier_2.12_1.0.0-M6/">sbt-coursier_2.12_1.0.0-M6/</a>\n' + ); + nock('https://repo.maven.apache.org') + .get('/maven2/io/get-coursier/sbt-coursier_2.12_1.0/') + .reply( + 200, + '<a href="2.0.0-RC2/">2.0.0-RC2/</a>\n' + + '<a href="2.0.0-RC6-1/">2.0.0-RC6-1/</a>\n' + + '<a href="2.0.0-RC6-2/">2.0.0-RC6-2/</a>\n' + + '<a href="2.0.0-RC6-6/">2.0.0-RC6-6/</a>\n' + ); + nock('https://repo.maven.apache.org') + .get( + '/maven2/io/get-coursier/sbt-coursier_2.12_1.0/2.0.0-RC6-6/sbt-coursier-2.0.0-RC6-6.pom' + ) + .reply( + 200, + '<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">\n' + + '<url>https://get-coursier.io/</url>\n' + + '<scm>\n' + + '<url>https://github.com/coursier/sbt-coursier</url>\n' + + '</scm>\n' + + '</project>\n' + ); }); afterEach(() => { @@ -145,5 +178,30 @@ describe('datasource/sbt', () => { releases: [{ version: '0.5.5' }], }); }); + + it('extracts URL from Maven POM file', async () => { + expect( + await getPkgReleases({ + versioning: mavenVersioning.id, + datasource: sbtPlugin.id, + depName: 'io.get-coursier:sbt-coursier', + registryUrls: [MAVEN_REPO], + }) + ).toEqual({ + dependencyUrl: + 'https://repo.maven.apache.org/maven2/io/get-coursier/sbt-coursier', + display: 'io.get-coursier:sbt-coursier', + group: 'io.get-coursier', + name: 'sbt-coursier', + releases: [ + { version: '2.0.0-RC2' }, + { version: '2.0.0-RC6-1' }, + { version: '2.0.0-RC6-2' }, + { version: '2.0.0-RC6-6' }, + ], + homepage: 'https://get-coursier.io/', + sourceUrl: 'https://github.com/coursier/sbt-coursier', + }); + }); }); }); diff --git a/lib/datasource/sbt-plugin/index.ts b/lib/datasource/sbt-plugin/index.ts index b4a5aea935695434e6ba742bb107f8335ccf9bcd..ac2b4579877c9906fa3b450ae0cd0796392e678e 100644 --- a/lib/datasource/sbt-plugin/index.ts +++ b/lib/datasource/sbt-plugin/index.ts @@ -2,7 +2,12 @@ import { logger } from '../../logger'; import { compare } from '../../versioning/maven/compare'; import { GetReleasesConfig, ReleaseResult } from '../common'; import { downloadHttpProtocol } from '../maven/util'; -import { resolvePackageReleases } from '../sbt-package'; +import { + getArtifactSubdirs, + getLatestVersion, + getPackageReleases, + getUrls, +} from '../sbt-package'; import { SBT_PLUGINS_REPO, parseIndexDir } from './util'; export const id = 'sbt-plugin'; @@ -58,7 +63,7 @@ async function resolvePluginReleases( return [...new Set(releases)].sort(compare); } } - return resolvePackageReleases(rootUrl, artifact, scalaVersion); + return null; } export async function getReleases({ @@ -78,16 +83,29 @@ export async function getReleases({ for (let idx = 0; idx < searchRoots.length; idx += 1) { const searchRoot = searchRoots[idx]; - const versions = await resolvePluginReleases( + let versions = await resolvePluginReleases( searchRoot, artifact, scalaVersion ); + let urls = {}; + + if (!versions?.length) { + const artifactSubdirs = await getArtifactSubdirs( + searchRoot, + artifact, + scalaVersion + ); + versions = await getPackageReleases(searchRoot, artifactSubdirs); + const latestVersion = getLatestVersion(versions); + urls = await getUrls(searchRoot, artifactSubdirs, latestVersion); + } const dependencyUrl = `${searchRoot}/${artifact}`; if (versions) { return { + ...urls, display: lookupName, group: groupId, name: artifactId,