diff --git a/lib/modules/datasource/deno/index.spec.ts b/lib/modules/datasource/deno/index.spec.ts index 11114db83c81c36eb4e3e379e2176f47a3279eb2..32958a9806aae6c94149dee59420068a8fbf3aa2 100644 --- a/lib/modules/datasource/deno/index.spec.ts +++ b/lib/modules/datasource/deno/index.spec.ts @@ -1,4 +1,6 @@ +import { ZodError } from 'zod'; import * as httpMock from '../../../../test/http-mock'; +import { logger } from '../../../../test/util'; import { DenoDatasource } from '.'; describe('modules/datasource/deno/index', () => { @@ -10,7 +12,7 @@ describe('modules/datasource/deno/index', () => { .scope(deno.defaultRegistryUrls[0]) .get('/v2/modules/std') .reply(200, { - versions: ['0.163.0', '0.162.0'], + versions: ['0.163.0', '0.162.0', '0.161.0'], tags: [{ value: 'top_5_percent', kind: 'popularity' }], }) .get('/v2/modules/std/0.163.0') @@ -32,7 +34,9 @@ describe('modules/datasource/deno/index', () => { type: 'github', }, uploaded_at: '2022-10-20T12:10:21.592Z', - }); + }) + .get('/v2/modules/std/0.161.0') + .reply(200, { foo: 'bar' }); const result = await deno.getReleases({ packageName: 'https://deno.land/std', @@ -50,11 +54,21 @@ describe('modules/datasource/deno/index', () => { sourceUrl: 'https://github.com/denoland/deno_std', releaseTimestamp: '2022-10-20T12:10:21.592Z', }, + { + version: '0.161.0', + }, ], tags: { popularity: 'top_5_percent', }, }); + + expect(logger.logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + err: expect.any(ZodError), + }), + `Deno: failed to get version details for 0.161.0` + ); }); it('throws error if module endpoint fails', async () => { diff --git a/lib/modules/datasource/deno/index.ts b/lib/modules/datasource/deno/index.ts index 81b42542b57e2d0cbf05362a829897d3d56e36ec..c0b63417fed450c2e7efbf85ee078f567f301fab 100644 --- a/lib/modules/datasource/deno/index.ts +++ b/lib/modules/datasource/deno/index.ts @@ -9,12 +9,7 @@ import * as semanticVersioning from '../../versioning/semver'; import { Datasource } from '../datasource'; import type { Release } from '../index'; import type { GetReleasesConfig, ReleaseResult } from '../types'; -import type { - DenoAPIModuleResponse, - DenoAPIModuleVersionResponse, - ReleaseMap, -} from './types'; -import { createSourceURL, tagsToRecord } from './utils'; +import { DenoAPIModuleResponse, DenoAPIModuleVersionResponse } from './schema'; export class DenoDatasource extends Datasource { static readonly id = 'deno'; @@ -45,7 +40,7 @@ export class DenoDatasource extends Datasource { const massagedRegistryUrl = registryUrl!; const extractResult = regEx( - '^(https://deno.land/)(?<rawPackageName>[^@\\s]+)' + /^(https:\/\/deno.land\/)(?<rawPackageName>[^@\s]+)/ ).exec(packageName); const rawPackageName = extractResult?.groups?.rawPackageName; if (is.nullOrUndefined(rawPackageName)) { @@ -73,17 +68,17 @@ export class DenoDatasource extends Datasource { key: (moduleAPIURL) => moduleAPIURL, }) async getReleaseResult(moduleAPIURL: string): Promise<ReleaseResult> { - const { versions, tags } = ( - await this.http.getJson<DenoAPIModuleResponse>(moduleAPIURL) - ).body; - - const releasesCache = - (await packageCache.get<ReleaseMap>( + const releasesCache: Record<string, Release> = + (await packageCache.get( `datasource-${DenoDatasource.id}-details`, moduleAPIURL )) ?? {}; let cacheModified = false; + const { + body: { versions, tags }, + } = await this.http.getJson(moduleAPIURL, DenoAPIModuleResponse); + // get details for the versions const releases = await pMap( versions, @@ -95,8 +90,16 @@ export class DenoDatasource extends Datasource { } // https://apiland.deno.dev/v2/modules/postgres/v0.17.0 - const release = await this.getReleaseDetails( - joinUrlParts(moduleAPIURL, version) + const url = joinUrlParts(moduleAPIURL, version); + const { body: release } = await this.http.getJson( + url, + DenoAPIModuleVersionResponse.catch(({ error: err }) => { + logger.warn( + { err }, + `Deno: failed to get version details for ${version}` + ); + return { version }; + }) ); releasesCache[release.version] = release; @@ -117,21 +120,6 @@ export class DenoDatasource extends Datasource { ); } - return { - releases, - tags: tagsToRecord(tags), - }; - } - - async getReleaseDetails(moduleAPIVersionURL: string): Promise<Release> { - const { version, uploaded_at, upload_options } = ( - await this.http.getJson<DenoAPIModuleVersionResponse>(moduleAPIVersionURL) - ).body; - return { - version, - gitRef: upload_options.ref, - releaseTimestamp: uploaded_at, - sourceUrl: createSourceURL(upload_options), - }; + return { releases, tags }; } } diff --git a/lib/modules/datasource/deno/schema.ts b/lib/modules/datasource/deno/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..412c379776e896c15f87ea5ac00cc1fc478906f3 --- /dev/null +++ b/lib/modules/datasource/deno/schema.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { getSourceUrl as getGithubSourceUrl } from '../../../util/github/url'; +import { looseArray } from '../../../util/schema-utils'; +import type { Release } from '../types'; + +export const DenoApiTag = z.object({ + kind: z.string(), + value: z.string(), +}); + +export const DenoAPIModuleResponse = z.object({ + tags: looseArray(DenoApiTag).transform((tags) => { + const record: Record<string, string> = {}; + for (const { kind, value } of tags) { + record[kind] = value; + } + return record; + }), + versions: z.array(z.string()), +}); + +export const DenoAPIUploadOptions = z.object({ + ref: z.string(), + type: z.union([z.literal('github'), z.unknown()]), + repository: z.string(), + subdir: z.string().optional(), +}); + +export const DenoAPIModuleVersionResponse = z + .object({ + upload_options: DenoAPIUploadOptions, + uploaded_at: z.string(), + version: z.string(), + }) + .transform( + ({ version, uploaded_at: releaseTimestamp, upload_options }): Release => { + let sourceUrl: string | undefined = undefined; + const { type, repository, ref: gitRef } = upload_options; + if (type === 'github') { + sourceUrl = getGithubSourceUrl(repository); + } + return { version, gitRef, releaseTimestamp, sourceUrl }; + } + ); diff --git a/lib/modules/datasource/deno/types.ts b/lib/modules/datasource/deno/types.ts deleted file mode 100644 index b130637b8f149885f7cc45922c23ba590457d591..0000000000000000000000000000000000000000 --- a/lib/modules/datasource/deno/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Release } from '../types'; - -export interface DenoAPIModuleResponse { - name: string; - latest_version: string; - star_count: number; - popularity_score: number; - tags: DenoAPITags[]; - versions: string[]; - description: string; -} - -export interface DenoAPIModuleVersionResponse { - upload_options: DenoAPIUploadOptions; - analysis_version: string; - description: string; - uploaded_at: string; // ISO date - name: string; - version: string; -} - -export interface DenoAPIUploadOptions { - ref: string; // commit ref / tag - type: 'github' | unknown; // type of hosting. seen: ['github'] - repository: string; // repo of hosting e.g. denodrivers/postgres - subdir?: string; -} - -export interface DenoAPITags { - kind: string; // e.g. popularity - value: string; // e.g. top_5_percent -} - -export type ReleaseMap = Record<string, Release>; diff --git a/lib/modules/datasource/deno/utils.ts b/lib/modules/datasource/deno/utils.ts deleted file mode 100644 index 322928c114b8b8ade3897503684033d897859018..0000000000000000000000000000000000000000 --- a/lib/modules/datasource/deno/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getSourceUrl as getGithubSourceUrl } from '../../../util/github/url'; -import type { DenoAPITags, DenoAPIUploadOptions } from './types'; - -export function createSourceURL({ - type, - repository, -}: DenoAPIUploadOptions): string | undefined { - switch (type) { - case 'github': - return getGithubSourceUrl(repository); - default: - return undefined; - } -} - -export function tagsToRecord(tags: DenoAPITags[]): Record<string, string> { - const record: Record<string, string> = {}; - for (const { kind, value } of tags) { - record[kind] = value; - } - return record; -}