From 0b73d683df24af2b0ac8a17afd6011843ce02801 Mon Sep 17 00:00:00 2001 From: Vincent Mahnke Date: Mon, 25 Mar 2024 21:45:13 +0100 Subject: [PATCH] feat(datasource): Add Unity3D (#27971) Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/api.ts | 2 + .../datasource/unity3d/__fixtures__/beta.xml | 21 ++ .../datasource/unity3d/__fixtures__/lts.xml | 21 ++ .../unity3d/__fixtures__/no_channel.xml | 4 + .../unity3d/__fixtures__/no_item.xml | 11 + .../unity3d/__fixtures__/no_title.xml | 20 ++ .../unity3d/__fixtures__/stable.xml | 31 ++ lib/modules/datasource/unity3d/index.spec.ts | 307 ++++++++++++++++++ lib/modules/datasource/unity3d/index.ts | 92 ++++++ lib/util/cache/package/types.ts | 1 + 10 files changed, 510 insertions(+) create mode 100644 lib/modules/datasource/unity3d/__fixtures__/beta.xml create mode 100644 lib/modules/datasource/unity3d/__fixtures__/lts.xml create mode 100644 lib/modules/datasource/unity3d/__fixtures__/no_channel.xml create mode 100644 lib/modules/datasource/unity3d/__fixtures__/no_item.xml create mode 100644 lib/modules/datasource/unity3d/__fixtures__/no_title.xml create mode 100644 lib/modules/datasource/unity3d/__fixtures__/stable.xml create mode 100644 lib/modules/datasource/unity3d/index.spec.ts create mode 100644 lib/modules/datasource/unity3d/index.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index 4ba206675060c8..e59f0b3709e68e 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -59,6 +59,7 @@ import { SbtPluginDatasource } from './sbt-plugin'; import { TerraformModuleDatasource } from './terraform-module'; import { TerraformProviderDatasource } from './terraform-provider'; import type { DatasourceApi } from './types'; +import { Unity3dDatasource } from './unity3d'; const api = new Map(); export default api; @@ -126,3 +127,4 @@ api.set(SbtPackageDatasource.id, new SbtPackageDatasource()); api.set(SbtPluginDatasource.id, new SbtPluginDatasource()); api.set(TerraformModuleDatasource.id, new TerraformModuleDatasource()); api.set(TerraformProviderDatasource.id, new TerraformProviderDatasource()); +api.set(Unity3dDatasource.id, new Unity3dDatasource()); diff --git a/lib/modules/datasource/unity3d/__fixtures__/beta.xml b/lib/modules/datasource/unity3d/__fixtures__/beta.xml new file mode 100644 index 00000000000000..ac53f15db16eff --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/beta.xml @@ -0,0 +1,21 @@ + + + + Latest Unity Beta Releases + https://unity.com/ + Latest Unity Beta Releases + en + + + 2023.3.0b6 + https://unity.com/releases/editor/beta/2023.3.0b6 + +Beta description + + 2024-02-07T07:24:40 + Unity Technologies + 4ca2224a582d + + + + diff --git a/lib/modules/datasource/unity3d/__fixtures__/lts.xml b/lib/modules/datasource/unity3d/__fixtures__/lts.xml new file mode 100644 index 00000000000000..dc273bf6c8c232 --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/lts.xml @@ -0,0 +1,21 @@ + + + + Latest Unity Lts Releases + https://unity.com/ + Latest Unity LTS Releases + en + + + 2021.3.35f1 + https://unity.com/releases/editor/whats-new/2021.3.35 + +LTS description + + 2024-02-06T15:40:15 + Unity Technologies + 157b46ce122a + + + + diff --git a/lib/modules/datasource/unity3d/__fixtures__/no_channel.xml b/lib/modules/datasource/unity3d/__fixtures__/no_channel.xml new file mode 100644 index 00000000000000..5afd7008d27baa --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/no_channel.xml @@ -0,0 +1,4 @@ + + + + diff --git a/lib/modules/datasource/unity3d/__fixtures__/no_item.xml b/lib/modules/datasource/unity3d/__fixtures__/no_item.xml new file mode 100644 index 00000000000000..604cbe1c269827 --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/no_item.xml @@ -0,0 +1,11 @@ + + + + Latest Unity Beta Releases + https://unity.com/ + Latest Unity Beta Releases + en + + + + diff --git a/lib/modules/datasource/unity3d/__fixtures__/no_title.xml b/lib/modules/datasource/unity3d/__fixtures__/no_title.xml new file mode 100644 index 00000000000000..35ac231d5b46fb --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/no_title.xml @@ -0,0 +1,20 @@ + + + + Latest Unity Full Releases + https://unity.com/ + Latest Unity Full Releases + en + + + https://unity.com/releases/editor/whats-new/2021.3.35 + +Stable description2 + + 2024-02-06T15:40:15 + Unity Technologies + 157b46ce122a + + + + diff --git a/lib/modules/datasource/unity3d/__fixtures__/stable.xml b/lib/modules/datasource/unity3d/__fixtures__/stable.xml new file mode 100644 index 00000000000000..268e62ddec6ce4 --- /dev/null +++ b/lib/modules/datasource/unity3d/__fixtures__/stable.xml @@ -0,0 +1,31 @@ + + + + Latest Unity Full Releases + https://unity.com/ + Latest Unity Full Releases + en + + + 2023.2.9f1 + https://unity.com/releases/editor/whats-new/2023.2.9 + +Stable description + + 2024-02-07T06:56:57 + Unity Technologies + 0c9c2e1f4bef + + + 2021.3.35f1 + https://unity.com/releases/editor/whats-new/2021.3.35 + +Stable description2 + + 2024-02-06T15:40:15 + Unity Technologies + 157b46ce122a + + + + diff --git a/lib/modules/datasource/unity3d/index.spec.ts b/lib/modules/datasource/unity3d/index.spec.ts new file mode 100644 index 00000000000000..b706c72a7b92c2 --- /dev/null +++ b/lib/modules/datasource/unity3d/index.spec.ts @@ -0,0 +1,307 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { Unity3dDatasource } from '.'; + +describe('modules/datasource/unity3d/index', () => { + const fixtures = Object.fromEntries( + [ + ...Object.keys(Unity3dDatasource.streams), + 'no_title', + 'no_channel', + 'no_item', + ].map((fixture) => [fixture, Fixtures.get(fixture + '.xml')]), + ); + + const mockRSSFeeds = (streams: { [keys: string]: string }) => { + Object.entries(streams).map(([stream, url]) => { + const content = fixtures[stream]; + + const uri = new URL(url); + + httpMock.scope(uri.origin).get(uri.pathname).reply(200, content); + }); + }; + + const stableStreamUrl = new URL(Unity3dDatasource.streams.stable); + + it('handle 500 response', async () => { + httpMock + .scope(stableStreamUrl.origin) + .get(stableStreamUrl.pathname) + .reply(500, '500'); + + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + const response = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(response).toBeNull(); + }); + + it('handle 200 with no XML', async () => { + httpMock + .scope(stableStreamUrl.origin) + .get(stableStreamUrl.pathname) + .reply(200, 'not xml'); + + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + const response = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(response).toBeNull(); + }); + + it('handles missing title element', async () => { + const content = fixtures.no_title; + httpMock + .scope(stableStreamUrl.origin) + .get(stableStreamUrl.pathname) + .reply(200, content); + + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(responses).toEqual({ + releases: [], + homepage: 'https://unity.com/', + registryUrl: Unity3dDatasource.streams.stable, + }); + }); + + it('handles missing channel element', async () => { + const content = fixtures.no_channel; + httpMock + .scope(stableStreamUrl.origin) + .get(stableStreamUrl.pathname) + .reply(200, content); + + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(responses).toEqual({ + releases: [], + homepage: 'https://unity.com/', + registryUrl: Unity3dDatasource.streams.stable, + }); + }); + + it('handles missing item element', async () => { + const content = fixtures.no_item; + httpMock + .scope(stableStreamUrl.origin) + .get(stableStreamUrl.pathname) + .reply(200, content); + + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(responses).toEqual({ + releases: [], + homepage: 'https://unity.com/', + registryUrl: Unity3dDatasource.streams.stable, + }); + }); + + it('returns beta if requested', async () => { + mockRSSFeeds({ beta: Unity3dDatasource.streams.beta }); + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.beta], + }); + + expect(responses).toEqual({ + registryUrl: Unity3dDatasource.streams.beta, + releases: [ + { + changelogUrl: 'https://unity.com/releases/editor/beta/2023.3.0b6', + isStable: false, + registryUrl: Unity3dDatasource.streams.beta, + releaseTimestamp: '2024-02-07T07:24:40.000Z', + version: '2023.3.0b6', + }, + ], + homepage: 'https://unity.com/', + }); + }); + + it('returns stable and lts releases by default', async () => { + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + mockRSSFeeds(qualifyingStreams); + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + }); + + expect(responses).toEqual( + expect.objectContaining({ + releases: [ + { + changelogUrl: + 'https://unity.com/releases/editor/whats-new/2021.3.35', + isStable: true, + registryUrl: Unity3dDatasource.streams.stable, + releaseTimestamp: '2024-02-06T15:40:15.000Z', + version: '2021.3.35f1', + }, + { + changelogUrl: + 'https://unity.com/releases/editor/whats-new/2023.2.9', + isStable: true, + registryUrl: Unity3dDatasource.streams.stable, + releaseTimestamp: '2024-02-07T06:56:57.000Z', + version: '2023.2.9f1', + }, + ], + homepage: 'https://unity.com/', + }), + ); + + expect(responses).toEqual( + expect.objectContaining({ + releases: expect.not.arrayContaining([ + expect.objectContaining({ + version: expect.stringMatching(/\(b\)/), + registryUrl: Unity3dDatasource.streams.beta, + }), + expect.objectContaining({ + version: expect.stringMatching(/\(b\)/), + registryUrl: Unity3dDatasource.streams.beta, + }), + ]), + homepage: 'https://unity.com/', + }), + ); + }); + + it('returns hash if requested', async () => { + mockRSSFeeds({ stable: Unity3dDatasource.streams.stable }); + const responsesWithHash = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersionWithRevision', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(responsesWithHash).toEqual( + expect.objectContaining({ + releases: expect.arrayContaining([ + expect.objectContaining({ + version: expect.stringMatching(/\(.*\)/), + }), + ]), + homepage: 'https://unity.com/', + registryUrl: Unity3dDatasource.streams.stable, + }), + ); + }); + + it('returns no hash if not requested', async () => { + mockRSSFeeds({ stable: Unity3dDatasource.streams.stable }); + const responsesWithoutHash = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams.stable], + }); + + expect(responsesWithoutHash).toEqual( + expect.objectContaining({ + releases: expect.not.arrayContaining([ + expect.objectContaining({ + version: expect.stringMatching(/\(.*\)/), + }), + ]), + homepage: 'https://unity.com/', + registryUrl: Unity3dDatasource.streams.stable, + }), + ); + }); + + it('returns different versions for each stream', async () => { + mockRSSFeeds(Unity3dDatasource.streams); + const responses: { [keys: string]: string[] } = Object.fromEntries( + await Promise.all( + Object.keys(Unity3dDatasource.streams).map(async (stream) => [ + stream, + ( + await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersion', + registryUrls: [Unity3dDatasource.streams[stream]], + }) + )?.releases.map((release) => release.version), + ]), + ), + ); + + // none of the items in responses.beta are in responses.stable or responses.lts + expect( + responses.beta.every( + (betaVersion) => + !responses.stable.includes(betaVersion) && + !responses.lts.includes(betaVersion), + ), + ).toBe(true); + // some items in responses.stable are in responses.lts + expect( + responses.stable.some((stableVersion) => + responses.lts.includes(stableVersion), + ), + ).toBe(true); + // not all items in responses.stable are in responses.lts + expect( + responses.stable.every((stableVersion) => + responses.lts.includes(stableVersion), + ), + ).toBe(false); + }); + + it('returns only lts and stable by default', async () => { + const qualifyingStreams = { ...Unity3dDatasource.streams }; + delete qualifyingStreams.beta; + mockRSSFeeds(qualifyingStreams); + const responses = await getPkgReleases({ + datasource: Unity3dDatasource.id, + packageName: 'm_EditorVersionWithRevision', + }); + + expect(responses).toEqual( + expect.objectContaining({ + releases: expect.arrayContaining([ + expect.objectContaining({ + version: expect.stringMatching(/[fp]/), + registryUrl: expect.stringMatching(/(releases|lts)/), + }), + expect.objectContaining({ + version: expect.stringMatching(/[fp]/), + registryUrl: expect.stringMatching(/(releases|lts)/), + }), + ]), + homepage: 'https://unity.com/', + }), + ); + }); +}); diff --git a/lib/modules/datasource/unity3d/index.ts b/lib/modules/datasource/unity3d/index.ts new file mode 100644 index 00000000000000..c69624a4f90cf3 --- /dev/null +++ b/lib/modules/datasource/unity3d/index.ts @@ -0,0 +1,92 @@ +import { XmlDocument } from 'xmldoc'; +import { logger } from '../../../logger'; +import { cache } from '../../../util/cache/package/decorator'; +import * as Unity3dVersioning from '../../versioning/unity3d'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; + +export class Unity3dDatasource extends Datasource { + static readonly homepage = 'https://unity.com/'; + static readonly streams: { [key: string]: string } = { + lts: `${Unity3dDatasource.homepage}releases/editor/lts-releases.xml`, + stable: `${Unity3dDatasource.homepage}releases/editor/releases.xml`, + beta: `${Unity3dDatasource.homepage}releases/editor/beta/latest.xml`, + }; + + static readonly id = 'unity3d'; + + override readonly defaultRegistryUrls = [ + Unity3dDatasource.streams.stable, + Unity3dDatasource.streams.lts, + ]; + + override readonly defaultVersioning = Unity3dVersioning.id; + + override readonly registryStrategy = 'merge'; + + constructor() { + super(Unity3dDatasource.id); + } + + async getByStream( + registryUrl: string | undefined, + withHash: boolean, + ): Promise { + let channel = null; + try { + const response = await this.http.get(registryUrl!); + const document = new XmlDocument(response.body); + channel = document.childNamed('channel'); + } catch (err) { + logger.error( + { err, registryUrl }, + 'Failed to request releases from Unity3d datasource', + ); + return null; + } + + if (!channel) { + return { + releases: [], + homepage: Unity3dDatasource.homepage, + registryUrl, + }; + } + const releases = channel + .childrenNamed('item') + .map((itemNode) => { + const versionWithHash = `${itemNode.childNamed('title')?.val} (${itemNode.childNamed('guid')?.val})`; + const versionWithoutHash = itemNode.childNamed('title')?.val; + const release: Release = { + version: withHash ? versionWithHash : versionWithoutHash!, + releaseTimestamp: itemNode.childNamed('pubDate')?.val, + changelogUrl: itemNode.childNamed('link')?.val, + isStable: registryUrl !== Unity3dDatasource.streams.beta, + registryUrl, + }; + return release; + }) + .filter((release) => !!release); + + return { + releases, + homepage: Unity3dDatasource.homepage, + registryUrl, + }; + } + + @cache({ + namespace: `datasource-${Unity3dDatasource.id}`, + key: ({ registryUrl, packageName }: GetReleasesConfig) => + `${registryUrl}:${packageName}`, + }) + async getReleases({ + packageName, + registryUrl, + }: GetReleasesConfig): Promise { + return await this.getByStream( + registryUrl, + packageName === 'm_EditorVersionWithRevision', + ); + } +} diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index b1e59a59f6ad3c..bb1673c399d8eb 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -100,6 +100,7 @@ export type PackageCacheNamespace = | 'datasource-terraform-provider-zip-hashes' | 'datasource-terraform-provider' | 'datasource-terraform' + | 'datasource-unity3d' | 'github-releases-datasource-v2' | 'github-tags-datasource-v2' | 'go'