Files
Sebastian Poxhofer 3ed45d1efd refactor(misc): remove getJsonUnchecked usages (#43696)
* fix(datasource): replace getJsonUnchecked with validated getJson in misc+merge-confidence group

Replaces all getJsonUnchecked calls with Zod-validated getJson calls in:
- crate: adds CrateMetadataSchema to existing schema.ts
- conan: uses existing ConanJSON schema directly
- puppet-forge: new schema.ts with PuppetModuleSchema
- conda: new schema.ts with CondaPackageSchema
- repology: new schema.ts with RepologyPackagesSchema
- jenkins-plugins: new schema.ts, parameterizes helper with ZodType<T>
- custom/formats/json: uses z.unknown() for user-defined responses
- merge-confidence: adds inline MergeConfidenceResponseSchema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: correct nullable/optional schema fields for crate, jenkins-plugins, puppet-forge

- crate: remove .optional() from all CrateMetadata fields to match string|null type
- jenkins-plugins: use .default({}) instead of .optional() for plugins fields so schemas match required Record types; widen ZodType input to unknown; update JenkinsPluginVersion.releaseTimestamp to Timestamp|null to match MaybeTimestamp output
- puppet-forge: change releases from .optional() to .default([]) to match Release[] type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: migrate zod/v3 imports to zod/v4 in misc datasources and merge-confidence

Update remaining zod/v3 imports in files added/modified by the getJsonUnchecked
migration to use zod/v4 as required after the upstream migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore minor improvements

* remove unnecessary types and reintroduce nullable where necessary

* refactor: move logic into transform

* test: add tests to improve coverage

* chore removed some unused types

* remove type declaration

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:58:20 +00:00

215 lines
6.7 KiB
TypeScript

import { isString, isUndefined } from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import { withCache } from '../../../util/cache/package/with-cache.ts';
import { GithubHttp } from '../../../util/http/github.ts';
import { regEx } from '../../../util/regex.ts';
import { ensureTrailingSlash, joinUrlParts } from '../../../util/url.ts';
import * as allVersioning from '../../versioning/index.ts';
import { Datasource } from '../datasource.ts';
import type {
DigestConfig,
GetReleasesConfig,
Release,
ReleaseResult,
} from '../types.ts';
import { isArtifactoryServer } from '../util.ts';
import { datasource, defaultRegistryUrl, getConanPackage } from './common.ts';
import {
ConanCenterReleases,
ConanJSON,
ConanLatestRevision,
ConanProperties,
ConanRevisionJSON,
} from './schema.ts';
export class ConanDatasource extends Datasource {
static readonly id = datasource;
override readonly defaultRegistryUrls = [defaultRegistryUrl];
override readonly caching = true;
override readonly registryStrategy = 'merge';
githubHttp: GithubHttp;
override readonly sourceUrlSupport = 'package';
override readonly sourceUrlNote =
'The source URL is supported only if the package is served from the Artifactory servers. In which case we determine it from the `properties[conan.package.url]` field in the results.';
constructor(id = ConanDatasource.id) {
super(id);
this.githubHttp = new GithubHttp(id);
}
async getConanCenterReleases(
conanName: string,
userAndChannel: string,
): Promise<ReleaseResult | null> {
if (userAndChannel && userAndChannel !== '@_/_') {
logger.debug(
{ conanName, userAndChannel },
'User/channel not supported for Conan Center lookups',
);
return null;
}
const url = `https://api.github.com/repos/conan-io/conan-center-index/contents/recipes/${conanName}/config.yml`;
const { body: result } = await this.githubHttp.getYaml(
url,
{ headers: { accept: 'application/vnd.github.v3.raw' } },
ConanCenterReleases,
);
return result;
}
private async _getDigest(
{ registryUrl, packageName }: DigestConfig,
newValue?: string,
): Promise<string | null> {
if (isUndefined(newValue) || isUndefined(registryUrl)) {
return null;
}
const url = ensureTrailingSlash(registryUrl);
const conanPackage = getConanPackage(packageName);
const revisionLookUp = joinUrlParts(
url,
'v2/conans/',
conanPackage.conanName,
newValue,
conanPackage.userAndChannel,
'/revisions',
);
const { body: digest } = await this.http.getJson(
revisionLookUp,
ConanLatestRevision,
);
return digest;
}
override getDigest(
config: DigestConfig,
newValue?: string,
): Promise<string | null> {
return withCache(
{
namespace: `datasource-${datasource}`,
// TODO: types (#22198)
key: `getDigest:${config.registryUrl!}:${config.packageName}:${newValue!}`,
fallback: true,
},
() => this._getDigest(config, newValue),
);
}
private async _getReleases({
registryUrl,
packageName,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const conanPackage = getConanPackage(packageName);
const userAndChannel = `@${conanPackage.userAndChannel}`;
if (
isString(registryUrl) &&
ensureTrailingSlash(registryUrl) === defaultRegistryUrl
) {
return this.getConanCenterReleases(
conanPackage.conanName,
userAndChannel,
);
}
logger.trace(
{ packageName, registryUrl },
'Looking up conan api dependency',
);
if (registryUrl) {
const url = ensureTrailingSlash(registryUrl);
const lookupUrl = joinUrlParts(
url,
`v2/conans/search?q=${conanPackage.conanName}`,
);
try {
const rep = await this.http.getJson(lookupUrl, ConanJSON);
const conanJson = rep.body;
if (conanJson) {
logger.trace({ lookupUrl }, 'Got conan api result');
const dep: ReleaseResult = { releases: [] };
const conanJsonReleases: Release[] = conanJson
.filter(({ userChannel }) => userChannel === userAndChannel)
.map(({ version }) => ({ version }));
dep.releases.push(...conanJsonReleases);
try {
if (isArtifactoryServer(rep)) {
const conanApiRegexp = regEx(
/(?<host>.*)\/artifactory\/api\/conan\/(?<repo>[^/]+)/,
);
const groups = conanApiRegexp.exec(url)?.groups;
if (!groups) {
return dep;
}
const semver = allVersioning.get('semver');
const sortedReleases = dep.releases
.filter((release) => semver.isVersion(release.version))
.sort((a, b) => semver.sortVersions(a.version, b.version));
const latestVersion = sortedReleases.at(-1)?.version;
if (!latestVersion) {
return dep;
}
logger.debug(
`Conan package ${packageName} has latest version ${latestVersion}`,
);
const latestRevisionUrl = joinUrlParts(
url,
`v2/conans/${conanPackage.conanName}/${latestVersion}/${conanPackage.userAndChannel}/latest`,
);
const {
body: { revision: packageRev },
} = await this.http.getJson(latestRevisionUrl, ConanRevisionJSON);
const [user, channel] = conanPackage.userAndChannel.split('/');
const packageUrl = joinUrlParts(
`${groups.host}/artifactory/api/storage/${groups.repo}`,
`${user}/${conanPackage.conanName}/${latestVersion}/${channel}/${packageRev}/export/conanfile.py?properties=conan.package.url`,
);
const { body: conanProperties } = await this.http.getJson(
packageUrl,
ConanProperties,
);
const { sourceUrl } = conanProperties;
if (sourceUrl) {
dep.sourceUrl = sourceUrl;
}
}
} catch (err) {
logger.debug({ err }, "Couldn't determine Conan package url");
}
return dep;
}
} catch (err) {
this.handleGenericErrors(err);
}
}
return null;
}
getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null> {
return withCache(
{
namespace: `datasource-${datasource}`,
// TODO: types (#22198)
key: `getReleases:${config.registryUrl}:${config.packageName}`,
fallback: true,
},
() => this._getReleases(config),
);
}
}