Files
renovate/lib/modules/datasource/java-version/index.spec.ts
Gregor Zeitlinger 0d9888a4f2 feat(datasource): add GraalVM version datasource (#40497)
* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases

* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases

* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases

* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases

* feat(datasource): add GraalVM version datasource with support for JDK and JRE releases

* add coverage

* fix

* fix

* fix

* code cov

* code cov

* code cov

* refactor(datasource): integrate GraalVM into java-version datasource

Merges graalvm-version datasource into java-version as a unified multi-vendor
Java distribution datasource. Implements vendor-based modular architecture with
routing layer for Adoptium (Eclipse Temurin) and Oracle GraalVM.

Architecture:
- Vendor-specific handlers (adoptium.ts, graalvm.ts) with routing in index.ts
- Vendor-aware system detection with different arch/os mappings per vendor
- Custom registry support for GraalVM's mise-java API
- Adoptium remains the default registry for backwards compatibility

Package names:
- New explicit: adoptium-jdk, adoptium-jre, oracle-graalvm-jdk, oracle-graalvm-jre
- Legacy (map to Adoptium): java, java-jdk, java-jre

Changes:
- Created adoptium.ts and graalvm.ts vendor handlers
- Enhanced common.ts with vendor detection and vendor-specific system mappings
- Updated index.ts with routing layer and custom registry support
- Merged test suites with comprehensive coverage for both vendors
- Updated mise manager integration to use JavaVersionDatasource
- Removed graalvm-version datasource directory

All 65 datasource tests and 93 mise manager tests passing.

* fix

* test(datasource): add coverage for non-HttpError paths in java-version

* fix(datasource): remove dead code and add coverage for java-version

Remove unreachable HttpError handling from _getReleases catch block
since both getAdoptiumReleases and getGraalvmReleases handle HttpError
internally. Add test for the graalvmRegistryUrl fallback when
registryUrl is undefined.

* fix(datasource): address review comments for GraalVM datasource

- Remove unused 'datasource-graalvm-version' cache namespace
- Convert MiseJavaRelease interface to Zod schema and derive type from it
- Use http.getJson() with the Zod schema in getGraalvmReleases instead
  of getJsonUnchecked, letting the schema handle invalid/empty responses
- Update getGraalvmReleases test mock to use getJson

* docs: remove unused datasource-graalvm-version from cacheTtlOverride docs

* fix(datasource): preserve Adoptium config typing

* fix(datasource): harden GraalVM release parsing

* fix(datasource): address GraalVM review feedback

* test: cover GraalVM mise package mapping

* test(datasource): cover default GraalVM registry fallback

* test(datasource): cover legacy registry resolution

* test(datasource): type legacy datasource mocks

* fix(datasource): harden GraalVM defaults and responses

* refactor(datasource): use function-only registry accessors

* test(datasource): cover registry accessor functions

* test: restore coverage exclusions

* chore: retrigger CI checks
2026-09-23 08:40:24 +00:00

604 lines
20 KiB
TypeScript

import { Fixtures } from '~test/fixtures.ts';
import * as httpMock from '~test/http-mock.ts';
import { partial } from '~test/util.ts';
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages.ts';
import type { Http } from '../../../util/http/index.ts';
import { range } from '../../../util/range.ts';
import { getPkgReleases } from '../index.ts';
import { adoptiumRegistryUrl } from './adoptium.ts';
import { datasource, defaultRegistryUrl, pageSize } from './common.ts';
import { getGraalvmReleases, graalvmRegistryUrl } from './graalvm.ts';
import { JavaVersionDatasource } from './index.ts';
function getPath(page: number, imageType = 'jdk', args = ''): string {
return `/v3/info/release_versions?page_size=${pageSize}&image_type=${imageType}&project=jdk&release_type=ga&sort_method=DATE&sort_order=DESC${args}&page=${page}`;
}
const oracleGraalvmJdkReleases = [
{
checksum:
'sha256:46ec9582ebe114f93470403f2cc123238ac0c7982129c358af7d8e1de52dd663',
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jdk',
java_version: '23.0.1',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/23/archive/graalvm-jdk-23.0.1_linux-x64_bin.tar.gz',
vendor: 'oracle-graalvm',
version: '23.0.1',
},
{
checksum:
'sha256:2d6696aa209daa098c51fefc51906aa7bf0dbe28dcc560ef738328352564181b',
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jdk',
java_version: '21.0.5',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/21/archive/graalvm-jdk-21.0.5_linux-x64_bin.tar.gz',
vendor: 'oracle-graalvm',
version: '21.0.5',
},
{
checksum: null,
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jdk',
java_version: '17.0.13',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz',
vendor: 'oracle-graalvm',
version: '17.0.13',
},
{
checksum:
'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
created_at: '2024-11-15T10:20:30.123456',
features: [],
file_type: 'tar.gz',
image_type: 'jdk',
java_version: '11.0.22',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/11/archive/graalvm-jdk-11.0.22_linux-x64_bin.tar.gz',
vendor: 'graalvm',
version: '11.0.22',
},
];
const oracleGraalvmJreReleases = [
{
checksum:
'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jre',
java_version: '21.0.5',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/21/archive/graalvm-jre-21.0.5_linux-x64_bin.tar.gz',
vendor: 'oracle-graalvm',
version: '21.0.5',
},
{
checksum: null,
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jre',
java_version: '17.0.13',
jvm_impl: 'hotspot',
url: 'https://download.oracle.com/graalvm/17/latest/graalvm-jre-17_linux-x64_bin.tar.gz',
vendor: 'oracle-graalvm',
version: '17.0.13',
},
];
const packageName = 'java';
describe('modules/datasource/java-version/index', () => {
describe('getReleases', () => {
it('throws for error', async () => {
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0))
.replyWithError('error');
await expect(
getPkgReleases({
datasource,
packageName,
}),
).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('returns null for 404', async () => {
httpMock.scope(defaultRegistryUrl).get(getPath(0)).reply(404);
await expect(
getPkgReleases({
datasource,
packageName,
}),
).resolves.toBeNull();
});
it('returns null for empty result', async () => {
httpMock.scope(defaultRegistryUrl).get(getPath(0)).reply(200, {});
await expect(
getPkgReleases({
datasource,
packageName,
}),
).resolves.toBeNull();
});
it('returns null for empty 200 OK', async () => {
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0))
.reply(200, { versions: [] });
await expect(
getPkgReleases({
datasource,
packageName,
}),
).resolves.toBeNull();
});
it('throws for 5xx', async () => {
httpMock.scope(defaultRegistryUrl).get(getPath(0)).reply(502);
await expect(
getPkgReleases({
datasource,
packageName,
}),
).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('processes real data', async () => {
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0))
.reply(200, Fixtures.get('page.json'));
const res = await getPkgReleases({
datasource,
packageName,
});
expect(res).toMatchObject({
releases: [
{ version: '8.0.302+8' },
{ version: '11.0.12+7' },
{ version: '16.0.2+7' },
],
});
});
it('processes real data (jre)', async () => {
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0, 'jre'))
.reply(200, Fixtures.get('jre.json'));
const res = await getPkgReleases({
datasource,
packageName: 'java-jre',
});
expect(res).toMatchObject({
releases: [{ version: '8.0.302+8' }, { version: '11.0.12+7' }],
});
});
it('processes real data (jre,windows,x64)', async () => {
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0, 'jre', '&os=windows&architecture=x64'))
.reply(200, Fixtures.get('jre.json'));
const res = await getPkgReleases({
datasource,
packageName: 'java-jre?os=windows&architecture=x64',
});
expect(res?.releases).toHaveLength(2);
});
it('pages', async () => {
const versions = [...range(1, 50)].map((v: number) => ({
semver: `1.${v}.0`,
}));
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0))
.reply(200, { versions })
.get(getPath(1))
.reply(404);
const res = await getPkgReleases({
datasource,
packageName,
});
expect(res).toMatchObject({
releases: Array.from({ length: 50 }, (_, idx) => ({
version: `1.${idx + 1}.0`,
})),
});
expect(res?.releases).toHaveLength(50);
});
it('processes real data (jre,system)', async () => {
vi.spyOn(process, 'arch', 'get').mockReturnValueOnce('ia32');
vi.spyOn(process, 'platform', 'get').mockReturnValueOnce('win32');
httpMock
.scope(defaultRegistryUrl)
.get(getPath(0, 'jre', '&os=windows&architecture=x86'))
.reply(200, Fixtures.get('jre.json'));
const res = await getPkgReleases({
datasource,
packageName: 'java-jre?system=true',
});
expect(res?.releases).toHaveLength(2);
});
describe('Oracle GraalVM', () => {
const graalvmBasePath = '/jvm/ga/linux/x86_64.json';
it('throws for network error', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.replyWithError('network error');
await expect(
getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
}),
).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('returns null for 404', async () => {
httpMock.scope(graalvmRegistryUrl).get(graalvmBasePath).reply(404);
await expect(
getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
}),
).resolves.toBeNull();
});
it('returns null for empty result', async () => {
httpMock.scope(graalvmRegistryUrl).get(graalvmBasePath).reply(200, []);
await expect(
getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
}),
).resolves.toBeNull();
});
it('throws for 5xx', async () => {
httpMock.scope(graalvmRegistryUrl).get(graalvmBasePath).reply(502);
await expect(
getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
}),
).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('processes real data for JDK', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
});
expect(res?.releases).toHaveLength(3);
expect(res?.homepage).toBe('https://www.oracle.com/java/graalvm/');
expect(res?.sourceUrl).toBe('https://github.com/oracle/graal');
const versions = res?.releases.map((r) => r.version);
expect(versions).toContain('23.0.1');
expect(versions).toContain('21.0.5');
expect(versions).toContain('17.0.13');
});
it('processes real data for JRE', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, oracleGraalvmJreReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jre?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
});
expect(res?.releases).toHaveLength(2);
});
it('uses system detection', async () => {
vi.spyOn(process, 'arch', 'get').mockReturnValueOnce('x64');
vi.spyOn(process, 'platform', 'get').mockReturnValueOnce('linux');
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?system=true',
registryUrls: [graalvmRegistryUrl],
});
expect(res?.releases).toHaveLength(3);
});
it('handles early access releases', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ea/linux/x86_64.json')
.reply(200, []);
const res = await getPkgReleases({
datasource,
packageName:
'oracle-graalvm-jdk?os=linux&architecture=x86_64&release-type=ea',
registryUrls: [graalvmRegistryUrl],
});
expect(res).toBeNull();
});
it('returns null when system detection fails', async () => {
// @ts-expect-error - test unsupported architecture
vi.spyOn(process, 'arch', 'get').mockReturnValueOnce('unsupported');
// @ts-expect-error - test unsupported platform
vi.spyOn(process, 'platform', 'get').mockReturnValueOnce('unsupported');
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?system=true',
});
expect(res).toBeNull();
});
it('returns null when only OS is missing', async () => {
vi.spyOn(process, 'arch', 'get').mockReturnValueOnce('x64');
// @ts-expect-error - test unsupported platform
vi.spyOn(process, 'platform', 'get').mockReturnValueOnce('unsupported');
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?system=true',
});
expect(res).toBeNull();
});
it('returns null when only architecture is missing', async () => {
// @ts-expect-error - test unsupported architecture
vi.spyOn(process, 'arch', 'get').mockReturnValueOnce('unsupported');
vi.spyOn(process, 'platform', 'get').mockReturnValueOnce('linux');
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?system=true',
});
expect(res).toBeNull();
});
it('filters by vendor correctly', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
});
expect(res?.releases).toHaveLength(3);
// The data has 4 items but only 3 have vendor=oracle-graalvm
const versions = res?.releases.map((r) => r.version);
expect(versions).toContain('23.0.1');
expect(versions).toContain('21.0.5');
expect(versions).toContain('17.0.13');
});
it('returns null for malformed releases instead of partial results', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ea/linux/x86_64.json')
.reply(200, [
oracleGraalvmJdkReleases[0],
{ image_type: 'jdk', vendor: 'oracle-graalvm' },
]);
await expect(
getPkgReleases({
datasource,
packageName:
'oracle-graalvm-jdk?os=linux&architecture=x86_64&release-type=ea',
registryUrls: [graalvmRegistryUrl],
}),
).resolves.toBeNull();
});
it('returns null for an undefined response body', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ea/linux/x86_64.json')
.reply(200, undefined);
await expect(
getPkgReleases({
datasource,
packageName:
'oracle-graalvm-jdk?os=linux&architecture=x86_64&release-type=ea',
registryUrls: [graalvmRegistryUrl],
}),
).resolves.toBeNull();
});
it('uses the default OS when it is omitted', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ga/linux/aarch64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?architecture=aarch64',
});
expect(res?.releases).toHaveLength(3);
});
it('uses the default architecture when it is omitted', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ga/windows/x86_64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=windows',
});
expect(res?.releases).toHaveLength(3);
});
it('supports custom registry URL', async () => {
const customUrl = 'https://custom-registry.example.com/';
httpMock
.scope(customUrl)
.get('/jvm/ga/linux/x86_64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [customUrl],
});
expect(res?.releases).toHaveLength(3);
expect(res?.registryUrl).toBe('https://custom-registry.example.com');
});
it('supports different custom registry URL for JRE releases', async () => {
const customUrl = 'https://mirror.company.org/graalvm/';
httpMock
.scope(customUrl)
.get('/jvm/ga/linux/x86_64.json')
.reply(200, oracleGraalvmJreReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jre?os=linux&architecture=x86_64',
registryUrls: [customUrl],
});
expect(res?.releases).toHaveLength(2);
expect(res?.registryUrl).toBe('https://mirror.company.org/graalvm');
const versions = res?.releases.map((r) => r.version);
expect(versions).toContain('21.0.5');
expect(versions).toContain('17.0.13');
});
it('returns null when all releases are filtered out', async () => {
// Mock data has vendor='graalvm' (not matching oracle-graalvm)
const nonMatchingReleases = [
{
checksum: 'sha256:abc',
created_at: '2025-03-28T22:04:32.319048',
features: [],
file_type: 'tar.gz',
image_type: 'jdk',
java_version: '11.0.22',
jvm_impl: 'hotspot',
url: 'https://example.com/graalvm.tar.gz',
vendor: 'graalvm',
version: '11.0.22',
},
];
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, nonMatchingReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
});
expect(res).toBeNull();
});
it('falls back to default graalvm registry when registryUrl is undefined', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ga/linux/x86_64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
});
expect(res?.releases).toHaveLength(3);
});
it('uses default platform for a bare GraalVM package', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ga/linux/x86_64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk',
});
expect(res?.releases).toHaveLength(3);
});
it('uses the default graalvm registry in the datasource directly', async () => {
httpMock
.scope(graalvmRegistryUrl)
.get('/jvm/ga/linux/x86_64.json')
.reply(200, oracleGraalvmJdkReleases);
const res = await new JavaVersionDatasource().getReleases({
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
});
expect(res?.releases).toHaveLength(3);
expect(res?.registryUrl).toBe(graalvmRegistryUrl);
});
it('uses adoptium registry for adoptium and graalvm registry for graalvm', async () => {
// Test that Adoptium uses adoptiumRegistryUrl
httpMock
.scope(adoptiumRegistryUrl)
.get(getPath(0))
.reply(200, Fixtures.get('page.json'));
const adoptiumRes = await getPkgReleases({
datasource,
packageName: 'adoptium-jdk',
});
expect(adoptiumRes?.releases).toHaveLength(3);
// Test that GraalVM uses graalvmRegistryUrl
httpMock
.scope(graalvmRegistryUrl)
.get(graalvmBasePath)
.reply(200, oracleGraalvmJdkReleases);
const graalvmRes = await getPkgReleases({
datasource,
packageName: 'oracle-graalvm-jdk?os=linux&architecture=x86_64',
registryUrls: [graalvmRegistryUrl],
});
expect(graalvmRes?.releases).toHaveLength(3);
});
});
});
describe('getGraalvmReleases', () => {
it('re-throws non-HttpError', async () => {
const mockHttp = {
getJson: vi.fn().mockRejectedValue(new Error('unexpected')),
};
await expect(
getGraalvmReleases(
partial<Http>(mockHttp),
{
vendor: 'oracle-graalvm',
imageType: 'jdk',
architecture: 'x86_64',
os: 'linux',
},
graalvmRegistryUrl,
),
).rejects.toThrow('unexpected');
});
});
});