diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index 1f1949acd4bfa8fd4f8fd100e52e0d654ec9474b..45592575d9f69b5f386c08a0949bcdbf113f3bc0 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -11,6 +11,7 @@ import { CondaDatasource } from './conda'; import { CpanDatasource } from './cpan'; import { CrateDatasource } from './crate'; import { DartDatasource } from './dart'; +import { DartVersionDatasource } from './dart-version'; import { DenoDatasource } from './deno'; import { DockerDatasource } from './docker'; import { DotnetDatasource } from './dotnet'; @@ -67,6 +68,7 @@ api.set(CondaDatasource.id, new CondaDatasource()); api.set(CpanDatasource.id, new CpanDatasource()); api.set(CrateDatasource.id, new CrateDatasource()); api.set(DartDatasource.id, new DartDatasource()); +api.set(DartVersionDatasource.id, new DartVersionDatasource()); api.set(DenoDatasource.id, new DenoDatasource()); api.set(DockerDatasource.id, new DockerDatasource()); api.set(DotnetDatasource.id, new DotnetDatasource()); diff --git a/lib/modules/datasource/dart-version/__fixtures__/beta.json b/lib/modules/datasource/dart-version/__fixtures__/beta.json new file mode 100644 index 0000000000000000000000000000000000000000..8e2c28bc7e9d8ea175cacc1f363c7c1cdbc876f7 --- /dev/null +++ b/lib/modules/datasource/dart-version/__fixtures__/beta.json @@ -0,0 +1,15 @@ +{ + "kind": "storage#objects", + "prefixes": [ + "channels/beta/release/2.17.0-69.2.beta/", + "channels/beta/release/2.17.0/", + "channels/beta/release/2.17.1/", + "channels/beta/release/2.18.0-271.8.beta/", + "channels/beta/release/2.18.0-44.1.beta/", + "channels/beta/release/2.18.0/", + "channels/beta/release/2.19.0-255.2.beta/", + "channels/beta/release/2.19.0-374.1.beta/", + "channels/beta/release/2.19.0-374.2.beta/", + "channels/beta/release/latest/" + ] +} diff --git a/lib/modules/datasource/dart-version/__fixtures__/dev.json b/lib/modules/datasource/dart-version/__fixtures__/dev.json new file mode 100644 index 0000000000000000000000000000000000000000..3fdc56abf1dd13188ad8145cdc6a57937dca046e --- /dev/null +++ b/lib/modules/datasource/dart-version/__fixtures__/dev.json @@ -0,0 +1,15 @@ +{ + "kind": "storage#objects", + "prefixes": [ + "channels/dev/release/2.17.0-7.0.dev/", + "channels/dev/release/2.17.0-85.0.dev/", + "channels/dev/release/2.17.0-91.0.dev/", + "channels/dev/release/2.18.0-82.0.dev/", + "channels/dev/release/2.18.0-9.0.dev/", + "channels/dev/release/2.18.0-99.0.dev/", + "channels/dev/release/2.19.0-59.0.dev/", + "channels/dev/release/2.19.0-70.0.dev/", + "channels/dev/release/2.19.0-81.0.dev/", + "channels/dev/release/latest/" + ] +} diff --git a/lib/modules/datasource/dart-version/__fixtures__/stable.json b/lib/modules/datasource/dart-version/__fixtures__/stable.json new file mode 100644 index 0000000000000000000000000000000000000000..f5bb8cab7e838d9af9b18857baf0c1bfaccb4331 --- /dev/null +++ b/lib/modules/datasource/dart-version/__fixtures__/stable.json @@ -0,0 +1,12 @@ +{ + "kind": "storage#objects", + "prefixes": [ + "channels/stable/release/2.17.5/", + "channels/stable/release/2.17.6/", + "channels/stable/release/2.17.7/", + "channels/stable/release/2.18.0/", + "channels/stable/release/2.18.4/", + "channels/stable/release/2.18.5/", + "channels/stable/release/latest/" + ] +} diff --git a/lib/modules/datasource/dart-version/index.spec.ts b/lib/modules/datasource/dart-version/index.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..798bab2496f07de88c75be954b61fb9f0519b2a7 --- /dev/null +++ b/lib/modules/datasource/dart-version/index.spec.ts @@ -0,0 +1,74 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages'; +import { DartVersionDatasource } from '.'; + +const baseUrl = 'https://storage.googleapis.com'; +const urlPath = + '/storage/v1/b/dart-archive/o?delimiter=%2F&prefix=channels%2Fstable%2Frelease%2F&alt=json'; +const datasource = DartVersionDatasource.id; +const depName = 'dart'; +const channels = ['stable', 'beta', 'dev']; + +describe('modules/datasource/dart-version/index', () => { + describe('getReleases', () => { + it('throws for 500', async () => { + httpMock.scope(baseUrl).get(urlPath).reply(500); + await expect( + getPkgReleases({ + datasource, + depName, + }) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + + it('returns null for error', async () => { + httpMock.scope(baseUrl).get(urlPath).replyWithError('error'); + expect( + await getPkgReleases({ + datasource, + depName, + }) + ).toBeNull(); + }); + + it('returns null for empty 200 OK', async () => { + httpMock.scope(baseUrl).get(urlPath).reply(200, []); + expect( + await getPkgReleases({ + datasource, + depName, + }) + ).toBeNull(); + }); + + it('processes real data', async () => { + for (const channel of channels) { + httpMock + .scope(baseUrl) + .get( + `/storage/v1/b/dart-archive/o?delimiter=%2F&prefix=channels%2F${channel}%2Frelease%2F&alt=json` + ) + .reply(200, Fixtures.get(`${channel}.json`)); + } + + const res = await getPkgReleases({ + datasource, + depName, + }); + + expect(res).toBeDefined(); + expect(res?.sourceUrl).toBe('https://github.com/dart-lang/sdk'); + expect(res?.releases).toHaveLength(21); + expect(res?.releases).toIncludeAllPartialMembers([ + { version: '2.18.0', isStable: true }, + { version: '2.17.7', isStable: true }, + { version: '2.19.0-374.2.beta', isStable: false }, + { version: '2.18.0-44.1.beta', isStable: false }, + { version: '2.19.0-81.0.dev', isStable: false }, + { version: '2.18.0-99.0.dev', isStable: false }, + ]); + }); + }); +}); diff --git a/lib/modules/datasource/dart-version/index.ts b/lib/modules/datasource/dart-version/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b350f562b2a26ee662d2fee5260da033790f9e4b --- /dev/null +++ b/lib/modules/datasource/dart-version/index.ts @@ -0,0 +1,80 @@ +import is from '@sindresorhus/is'; +import { regEx } from '../../../util/regex'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import type { DartResponse } from './types'; + +export const stableVersionRegex = regEx(/^\d+\.\d+\.\d+$/); + +export class DartVersionDatasource extends Datasource { + static readonly id = 'dart-version'; + + constructor() { + super(DartVersionDatasource.id); + } + + override readonly customRegistrySupport = false; + + override readonly defaultRegistryUrls = ['https://storage.googleapis.com']; + + override readonly caching = true; + + private readonly channels = ['stable', 'beta', 'dev']; + + async getReleases({ + registryUrl, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + // istanbul ignore if + if (!registryUrl) { + return null; + } + const result: ReleaseResult = { + homepage: 'https://dart.dev/', + sourceUrl: 'https://github.com/dart-lang/sdk', + registryUrl, + releases: [], + }; + try { + for (const channel of this.channels) { + const resp = ( + await this.http.getJson<DartResponse>( + `${registryUrl}/storage/v1/b/dart-archive/o?delimiter=%2F&prefix=channels%2F${channel}%2Frelease%2F&alt=json` + ) + ).body; + const releases = this.getReleasesFromResponse(channel, resp.prefixes); + result.releases.push(...releases); + } + } catch (err) { + this.handleGenericErrors(err); + } + + return result.releases.length ? result : null; + } + + private getReleasesFromResponse( + channel: string, + prefixes: string[] + ): Release[] { + return prefixes + .map((prefix) => this.getVersionFromPrefix(prefix)) + .filter(is.string) + .filter((version) => { + if ( + version === 'latest' || + // The API response contains a stable version being released as a non-stable + // release. So we filter out these releases here. + (channel !== 'stable' && stableVersionRegex.test(version)) + ) { + return false; + } + return true; + }) + .map((version) => ({ version, isStable: channel === 'stable' })); + } + + // Prefix should have a format of "channels/stable/release/2.9.3/" + private getVersionFromPrefix(prefix: string): string | undefined { + const parts = prefix.split('/'); + return parts[parts.length - 2]; + } +} diff --git a/lib/modules/datasource/dart-version/types.ts b/lib/modules/datasource/dart-version/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..8061750f2e4595fce991626b6046edcd767be973 --- /dev/null +++ b/lib/modules/datasource/dart-version/types.ts @@ -0,0 +1,4 @@ +export interface DartResponse { + kind: string; + prefixes: string[]; +}