Skip to content

Commit

Permalink
feat: add bazel datasource (#21733)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgrindel committed Apr 25, 2023
1 parent 5ac2023 commit 7f4c8eb
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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());
Expand Down
@@ -0,0 +1,9 @@
{
"versions": [
"0.14.8",
"0.14.9",
"0.15.0",
"0.16.0"
],
"yanked_versions": {}
}
@@ -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."
}
}
82 changes: 82 additions & 0 deletions 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' },
],
});
});
});
});
70 changes: 70 additions & 0 deletions 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<ReleaseResult | null> {
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;
}
}
1 change: 1 addition & 0 deletions 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.
12 changes: 12 additions & 0 deletions 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);
});
});
});
6 changes: 6 additions & 0 deletions 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()),
});
2 changes: 2 additions & 0 deletions lib/modules/versioning/bazel-module/bzlmod-version.ts
Expand Up @@ -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;
Expand All @@ -214,6 +215,7 @@ export class BzlmodVersion {
* values.
*/
constructor(version: string) {
this.original = version;
if (version === '') {
this.release = VersionPart.create();
this.prerelease = VersionPart.create();
Expand Down

0 comments on commit 7f4c8eb

Please sign in to comment.