From 7f4c8ebeb919091b4773c177fa30f96873c6ac95 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 25 Apr 2023 14:08:26 -0600 Subject: [PATCH] feat: add `bazel` datasource (#21733) --- lib/modules/datasource/api.ts | 2 + .../metadata-no-yanked-versions.json | 9 ++ .../metadata-with-yanked-versions.json | 11 +++ lib/modules/datasource/bazel/index.spec.ts | 82 +++++++++++++++++++ lib/modules/datasource/bazel/index.ts | 70 ++++++++++++++++ lib/modules/datasource/bazel/readme.md | 1 + lib/modules/datasource/bazel/schema.spec.ts | 12 +++ lib/modules/datasource/bazel/schema.ts | 6 ++ .../versioning/bazel-module/bzlmod-version.ts | 2 + 9 files changed, 195 insertions(+) create mode 100644 lib/modules/datasource/bazel/__fixtures__/metadata-no-yanked-versions.json create mode 100644 lib/modules/datasource/bazel/__fixtures__/metadata-with-yanked-versions.json create mode 100644 lib/modules/datasource/bazel/index.spec.ts create mode 100644 lib/modules/datasource/bazel/index.ts create mode 100644 lib/modules/datasource/bazel/readme.md create mode 100644 lib/modules/datasource/bazel/schema.spec.ts create mode 100644 lib/modules/datasource/bazel/schema.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index 3048b2eb4e5a8b..e75ad8aa10bf9c 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -3,6 +3,7 @@ import { AwsMachineImageDataSource } from './aws-machine-image'; import { AwsRdsDataSource } from './aws-rds'; import { AzureBicepResourceDatasource } from './azure-bicep-resource'; import { AzurePipelinesTasksDatasource } from './azure-pipelines-tasks'; +import { BazelDatasource } from './bazel'; import { BitbucketTagsDatasource } from './bitbucket-tags'; import { CdnJsDatasource } from './cdnjs'; import { ClojureDatasource } from './clojure'; @@ -62,6 +63,7 @@ api.set(AwsMachineImageDataSource.id, new AwsMachineImageDataSource()); api.set(AwsRdsDataSource.id, new AwsRdsDataSource()); api.set(AzureBicepResourceDatasource.id, new AzureBicepResourceDatasource()); api.set(AzurePipelinesTasksDatasource.id, new AzurePipelinesTasksDatasource()); +api.set(BazelDatasource.id, new BazelDatasource()); api.set(BitbucketTagsDatasource.id, new BitbucketTagsDatasource()); api.set(CdnJsDatasource.id, new CdnJsDatasource()); api.set(ClojureDatasource.id, new ClojureDatasource()); diff --git a/lib/modules/datasource/bazel/__fixtures__/metadata-no-yanked-versions.json b/lib/modules/datasource/bazel/__fixtures__/metadata-no-yanked-versions.json new file mode 100644 index 00000000000000..2bbf5b1186fc61 --- /dev/null +++ b/lib/modules/datasource/bazel/__fixtures__/metadata-no-yanked-versions.json @@ -0,0 +1,9 @@ +{ + "versions": [ + "0.14.8", + "0.14.9", + "0.15.0", + "0.16.0" + ], + "yanked_versions": {} +} diff --git a/lib/modules/datasource/bazel/__fixtures__/metadata-with-yanked-versions.json b/lib/modules/datasource/bazel/__fixtures__/metadata-with-yanked-versions.json new file mode 100644 index 00000000000000..213792c075a86a --- /dev/null +++ b/lib/modules/datasource/bazel/__fixtures__/metadata-with-yanked-versions.json @@ -0,0 +1,11 @@ +{ + "versions": [ + "0.14.8", + "0.14.9", + "0.15.0", + "0.16.0" + ], + "yanked_versions": { + "0.15.0": "Very bad bug." + } +} diff --git a/lib/modules/datasource/bazel/index.spec.ts b/lib/modules/datasource/bazel/index.spec.ts new file mode 100644 index 00000000000000..c9ba6ebb67437f --- /dev/null +++ b/lib/modules/datasource/bazel/index.spec.ts @@ -0,0 +1,82 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages'; +import { BazelDatasource } from '.'; + +const datasource = BazelDatasource.id; +const defaultRegistryUrl = BazelDatasource.bazelCentralRepoUrl; +const packageName = 'rules_foo'; +const path = BazelDatasource.packageMetadataPath(packageName); + +describe('modules/datasource/bazel/index', () => { + describe('getReleases', () => { + it('throws for error', async () => { + httpMock.scope(defaultRegistryUrl).get(path).replyWithError('error'); + await expect(getPkgReleases({ datasource, packageName })).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + }); + + it('returns null for 404', async () => { + httpMock.scope(defaultRegistryUrl).get(path).reply(404); + expect(await getPkgReleases({ datasource, packageName })).toBeNull(); + }); + + it('returns null for empty result', async () => { + httpMock.scope(defaultRegistryUrl).get(path).reply(200, {}); + expect(await getPkgReleases({ datasource, packageName })).toBeNull(); + }); + + it('returns null for empty 200 OK', async () => { + httpMock + .scope(defaultRegistryUrl) + .get(path) + .reply(200, '{ "versions": [], "yanked_versions": {} }'); + expect(await getPkgReleases({ datasource, packageName })).toBeNull(); + }); + + it('throws for 5xx', async () => { + httpMock.scope(defaultRegistryUrl).get(path).reply(502); + await expect(getPkgReleases({ datasource, packageName })).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + }); + + it('metadata without yanked versions', async () => { + httpMock + .scope(defaultRegistryUrl) + .get(path) + .reply(200, Fixtures.get('metadata-no-yanked-versions.json')); + const res = await getPkgReleases({ datasource, packageName }); + expect(res).toEqual({ + registryUrl: + 'https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main', + releases: [ + { version: '0.14.8' }, + { version: '0.14.9' }, + { version: '0.15.0' }, + { version: '0.16.0' }, + ], + }); + }); + + it('metadata with yanked versions', async () => { + httpMock + .scope(defaultRegistryUrl) + .get(path) + .reply(200, Fixtures.get('metadata-with-yanked-versions.json')); + const res = await getPkgReleases({ datasource, packageName }); + expect(res).toEqual({ + registryUrl: + 'https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main', + releases: [ + { version: '0.14.8' }, + { version: '0.14.9' }, + { version: '0.15.0', isDeprecated: true }, + { version: '0.16.0' }, + ], + }); + }); + }); +}); diff --git a/lib/modules/datasource/bazel/index.ts b/lib/modules/datasource/bazel/index.ts new file mode 100644 index 00000000000000..2a15a0eb84f830 --- /dev/null +++ b/lib/modules/datasource/bazel/index.ts @@ -0,0 +1,70 @@ +import is from '@sindresorhus/is'; +import { ExternalHostError } from '../../../types/errors/external-host-error'; +import { cache } from '../../../util/cache/package/decorator'; +import { HttpError } from '../../../util/http'; +import { joinUrlParts } from '../../../util/url'; +import { BzlmodVersion } from '../../versioning/bazel-module/bzlmod-version'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import { BazelModuleMetadata } from './schema'; + +export class BazelDatasource extends Datasource { + static readonly id = 'bazel'; + + static readonly bazelCentralRepoUrl = + 'https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main'; + + override readonly defaultRegistryUrls = [BazelDatasource.bazelCentralRepoUrl]; + override readonly customRegistrySupport = true; + override readonly caching = true; + + static packageMetadataPath(packageName: string): string { + return `/modules/${packageName}/metadata.json`; + } + + constructor() { + super(BazelDatasource.id); + } + + @cache({ + namespace: `datasource-${BazelDatasource.id}`, + key: ({ registryUrl, packageName }: GetReleasesConfig) => + `${registryUrl!}:${packageName}`, + }) + async getReleases({ + registryUrl, + packageName, + }: GetReleasesConfig): Promise { + const path = BazelDatasource.packageMetadataPath(packageName); + const url = joinUrlParts(registryUrl!, path); + + const result: ReleaseResult = { releases: [] }; + try { + const { body: metadata } = await this.http.getJson( + url, + BazelModuleMetadata + ); + result.releases = metadata.versions + .map((v) => new BzlmodVersion(v)) + .sort(BzlmodVersion.defaultCompare) + .map((bv) => { + const release: Release = { version: bv.original }; + if (is.truthy(metadata.yanked_versions[bv.original])) { + release.isDeprecated = true; + } + return release; + }); + } catch (err) { + // istanbul ignore else: not testable with nock + if (err instanceof HttpError) { + if (err.response?.statusCode === 404) { + return null; + } + throw new ExternalHostError(err); + } + this.handleGenericErrors(err); + } + + return result.releases.length ? result : null; + } +} diff --git a/lib/modules/datasource/bazel/readme.md b/lib/modules/datasource/bazel/readme.md new file mode 100644 index 00000000000000..e4282e3d45c672 --- /dev/null +++ b/lib/modules/datasource/bazel/readme.md @@ -0,0 +1 @@ +The `bazel` datasource is designed to query one or more [Bazel registries](https://bazel.build/external/registry) using the first successful result. diff --git a/lib/modules/datasource/bazel/schema.spec.ts b/lib/modules/datasource/bazel/schema.spec.ts new file mode 100644 index 00000000000000..832133baf11ab7 --- /dev/null +++ b/lib/modules/datasource/bazel/schema.spec.ts @@ -0,0 +1,12 @@ +import { Fixtures } from '../../../../test/fixtures'; +import { BazelModuleMetadata } from './schema'; + +describe('modules/datasource/bazel/schema', () => { + describe('BazelModuleMetadata', () => { + it('parses metadata', () => { + const metadataJson = Fixtures.get('metadata-with-yanked-versions.json'); + const metadata = BazelModuleMetadata.parse(JSON.parse(metadataJson)); + expect(metadata.versions).toHaveLength(4); + }); + }); +}); diff --git a/lib/modules/datasource/bazel/schema.ts b/lib/modules/datasource/bazel/schema.ts new file mode 100644 index 00000000000000..4315d260c4c93a --- /dev/null +++ b/lib/modules/datasource/bazel/schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const BazelModuleMetadata = z.object({ + versions: z.array(z.string()), + yanked_versions: z.record(z.string(), z.string()), +}); diff --git a/lib/modules/versioning/bazel-module/bzlmod-version.ts b/lib/modules/versioning/bazel-module/bzlmod-version.ts index dcc52d6e690564..42b315ed6724ff 100644 --- a/lib/modules/versioning/bazel-module/bzlmod-version.ts +++ b/lib/modules/versioning/bazel-module/bzlmod-version.ts @@ -199,6 +199,7 @@ interface VersionRegexResult { * It signifies that there is a NonRegistryOverride for a module. */ export class BzlmodVersion { + readonly original: string; readonly release: VersionPart; readonly prerelease: VersionPart; readonly build: VersionPart; @@ -214,6 +215,7 @@ export class BzlmodVersion { * values. */ constructor(version: string) { + this.original = version; if (version === '') { this.release = VersionPart.create(); this.prerelease = VersionPart.create();