From d63ff715cd72891c30faf4c61e9645bff5422e10 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Fri, 13 Dec 2024 01:17:26 -0600 Subject: [PATCH] feat(datasource): add Hackage datasource (#32944) Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/api.ts | 2 + lib/modules/datasource/hackage/index.spec.ts | 57 ++++++++++++++++++++ lib/modules/datasource/hackage/index.ts | 54 +++++++++++++++++++ lib/modules/datasource/hackage/readme.md | 7 +++ lib/modules/datasource/hackage/schema.ts | 3 ++ 5 files changed, 123 insertions(+) create mode 100644 lib/modules/datasource/hackage/index.spec.ts create mode 100644 lib/modules/datasource/hackage/index.ts create mode 100644 lib/modules/datasource/hackage/readme.md create mode 100644 lib/modules/datasource/hackage/schema.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index a9839f4f7a11de..e7284442c0a36b 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -38,6 +38,7 @@ import { GlasskubePackagesDatasource } from './glasskube-packages'; import { GoDatasource } from './go'; import { GolangVersionDatasource } from './golang-version'; import { GradleVersionDatasource } from './gradle-version'; +import { HackageDatasource } from './hackage'; import { HelmDatasource } from './helm'; import { HermitDatasource } from './hermit'; import { HexDatasource } from './hex'; @@ -111,6 +112,7 @@ api.set(GlasskubePackagesDatasource.id, new GlasskubePackagesDatasource()); api.set(GoDatasource.id, new GoDatasource()); api.set(GolangVersionDatasource.id, new GolangVersionDatasource()); api.set(GradleVersionDatasource.id, new GradleVersionDatasource()); +api.set(HackageDatasource.id, new HackageDatasource()); api.set(HelmDatasource.id, new HelmDatasource()); api.set(HermitDatasource.id, new HermitDatasource()); api.set(HexDatasource.id, new HexDatasource()); diff --git a/lib/modules/datasource/hackage/index.spec.ts b/lib/modules/datasource/hackage/index.spec.ts new file mode 100644 index 00000000000000..676e082583da48 --- /dev/null +++ b/lib/modules/datasource/hackage/index.spec.ts @@ -0,0 +1,57 @@ +import { getPkgReleases } from '..'; +import * as httpMock from '../../../../test/http-mock'; +import { HackageDatasource, versionToRelease } from './index'; + +const baseUrl = 'https://hackage.haskell.org/'; + +describe('modules/datasource/hackage/index', () => { + describe('versionToRelease', () => { + it('should make release with given version', () => { + expect( + versionToRelease('3.1.0', 'base', 'http://localhost').version, + ).toBe('3.1.0'); + }); + }); + + describe('getReleases', () => { + it('return null with empty registryUrl', async () => { + expect( + await new HackageDatasource().getReleases({ + packageName: 'base', + registryUrl: undefined, + }), + ).toBeNull(); + }); + + it('returns null for 404', async () => { + httpMock.scope(baseUrl).get('/package/base.json').reply(404); + expect( + await getPkgReleases({ + datasource: HackageDatasource.id, + packageName: 'base', + }), + ).toBeNull(); + }); + + it('returns release for 200', async () => { + httpMock + .scope(baseUrl) + .get('/package/base.json') + .reply(200, { '4.20.0.1': 'normal' }); + expect( + await getPkgReleases({ + datasource: HackageDatasource.id, + packageName: 'base', + }), + ).toEqual({ + registryUrl: baseUrl, + releases: [ + { + changelogUrl: baseUrl + 'package/base-4.20.0.1/changelog', + version: '4.20.0.1', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/datasource/hackage/index.ts b/lib/modules/datasource/hackage/index.ts new file mode 100644 index 00000000000000..4a75568d485468 --- /dev/null +++ b/lib/modules/datasource/hackage/index.ts @@ -0,0 +1,54 @@ +import is from '@sindresorhus/is'; +import { joinUrlParts } from '../../../util/url'; +import * as pvpVersioning from '../../versioning/pvp'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import { HackagePackageMetadata } from './schema'; + +export class HackageDatasource extends Datasource { + static readonly id = 'hackage'; + + constructor() { + super(HackageDatasource.id); + } + + override readonly defaultVersioning = pvpVersioning.id; + override readonly customRegistrySupport = false; + override readonly defaultRegistryUrls = ['https://hackage.haskell.org/']; + + async getReleases(config: GetReleasesConfig): Promise { + const { registryUrl, packageName } = config; + if (!is.nonEmptyString(registryUrl)) { + return null; + } + const massagedPackageName = encodeURIComponent(packageName); + const url = joinUrlParts( + registryUrl, + 'package', + `${massagedPackageName}.json`, + ); + const res = await this.http.getJson(url, HackagePackageMetadata); + const keys = Object.keys(res.body); + return { + releases: keys.map((version) => + versionToRelease(version, packageName, registryUrl), + ), + }; + } +} + +export function versionToRelease( + version: string, + packageName: string, + registryUrl: string, +): Release { + return { + version, + changelogUrl: joinUrlParts( + registryUrl, + 'package', + `${packageName}-${version}`, + 'changelog', + ), + }; +} diff --git a/lib/modules/datasource/hackage/readme.md b/lib/modules/datasource/hackage/readme.md new file mode 100644 index 00000000000000..d7e56e14b844c1 --- /dev/null +++ b/lib/modules/datasource/hackage/readme.md @@ -0,0 +1,7 @@ +This datasource uses +[the Hackage JSON API](https://hackage.haskell.org/api#package-info-json) +to fetch versions for published Haskell packages. + +While not all versions use [PVP](https://pvp.haskell.org), the majority does. +This manager assumes a default versioning set to PVP. +Versioning can be overwritten using `packageRules`, e.g. with `matchDatasources`. diff --git a/lib/modules/datasource/hackage/schema.ts b/lib/modules/datasource/hackage/schema.ts new file mode 100644 index 00000000000000..dcee186743c602 --- /dev/null +++ b/lib/modules/datasource/hackage/schema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const HackagePackageMetadata = z.record(z.string());