mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
ac22678812
* added support for extracting integrity and tarball from npm datasource * test(datasource/npm): keep integrity and tarball in packument shape assertions The packument-trimming tests on main pinned a cached shape that dropped `dist.tarball`, so they broke once the schema started retaining `integrity` and `tarball`. Update both expectations to the new shape and cover `integrity` alongside `tarball`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(workers/lookup): expect integrity and tarball on npm updates `generateUpdate` copies `downloadUrl` and `newDigest` from the release onto the update, so every npm lookup in these tests now carries the tarball URL and, where the packument fixture has one, the integrity hash. Pin and rollback updates are built without those release fields, so their expectations are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Michael Kriese <michael.kriese@mend.io> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
import { isNonEmptyString, isString } from '@sindresorhus/is';
|
|
import { z } from 'zod/v4';
|
|
import {
|
|
HOST_BLOCKED,
|
|
HOST_DISABLED,
|
|
} from '../../../constants/error-messages.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { ExternalHostError } from '../../../types/errors/external-host-error.ts';
|
|
import * as hostRules from '../../../util/host-rules.ts';
|
|
import { PackageHttpCacheProvider } from '../../../util/http/cache/package-http-cache-provider.ts';
|
|
import type { Http } from '../../../util/http/index.ts';
|
|
import type { HttpOptions } from '../../../util/http/types.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { DeepNullish } from '../../../util/schema-utils/index.ts';
|
|
import { asTimestamp } from '../../../util/timestamp.ts';
|
|
import { joinUrlParts } from '../../../util/url.ts';
|
|
import type { Release, ReleaseResult } from '../types.ts';
|
|
import { defaultRegistryUrl } from './common.ts';
|
|
import { CachedPackument, NpmResponse } from './schema.ts';
|
|
|
|
const SHORT_REPO_REGEX = regEx(
|
|
/^(?:(?<platform>bitbucket|github|gitlab):)?(?<shortRepo>[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/,
|
|
);
|
|
|
|
const platformMapping: Record<string, string> = {
|
|
bitbucket: 'https://bitbucket.org/',
|
|
github: 'https://github.com/',
|
|
gitlab: 'https://gitlab.com/',
|
|
};
|
|
|
|
interface PackageSource {
|
|
sourceUrl: string | null;
|
|
sourceDirectory: string | null;
|
|
}
|
|
|
|
const PackageSource = z
|
|
.union([
|
|
z
|
|
.string()
|
|
.nonempty()
|
|
.transform((repository): PackageSource => {
|
|
let sourceUrl: string | null = null;
|
|
const sourceDirectory = null;
|
|
const shortMatch = repository.match(SHORT_REPO_REGEX);
|
|
if (shortMatch?.groups) {
|
|
const { platform = 'github', shortRepo } = shortMatch.groups;
|
|
sourceUrl = platformMapping[platform] + shortRepo;
|
|
} else {
|
|
sourceUrl = repository;
|
|
}
|
|
return { sourceUrl, sourceDirectory };
|
|
}),
|
|
DeepNullish(
|
|
z.object({
|
|
url: z.string().nonempty().optional(),
|
|
directory: z.string().nonempty().optional(),
|
|
}),
|
|
).transform(({ url, directory }) => {
|
|
const res: PackageSource = { sourceUrl: null, sourceDirectory: null };
|
|
|
|
if (url) {
|
|
res.sourceUrl = url;
|
|
}
|
|
|
|
if (directory) {
|
|
res.sourceDirectory = directory;
|
|
}
|
|
|
|
return res;
|
|
}),
|
|
])
|
|
.catch({ sourceUrl: null, sourceDirectory: null });
|
|
|
|
export async function getDependency(
|
|
http: Http,
|
|
registryUrl: string,
|
|
packageName: string,
|
|
): Promise<ReleaseResult | null> {
|
|
logger.trace(`npm.getDependency(${packageName})`);
|
|
|
|
const packageUrl = joinUrlParts(registryUrl, packageName.replace('/', '%2F'));
|
|
|
|
try {
|
|
const cacheProvider = new PackageHttpCacheProvider({
|
|
namespace: 'datasource-npm:cache-provider',
|
|
checkAuthorizationHeader: false, // We don't rely on whether user token is provided or not
|
|
checkCacheControlHeader: true,
|
|
writeSchema: CachedPackument,
|
|
});
|
|
const options: HttpOptions = { cacheProvider };
|
|
|
|
// set abortOnError for registry.npmjs.org if no hostRule with explicit abortOnError exists
|
|
if (
|
|
registryUrl === defaultRegistryUrl &&
|
|
hostRules.find({ url: defaultRegistryUrl })?.abortOnError === undefined
|
|
) {
|
|
logger.trace(
|
|
{ packageName, registry: defaultRegistryUrl },
|
|
'setting abortOnError hostRule for well known host',
|
|
);
|
|
hostRules.add({
|
|
matchHost: defaultRegistryUrl,
|
|
abortOnError: true,
|
|
});
|
|
}
|
|
|
|
const resp = await http.getJson(packageUrl, options, NpmResponse);
|
|
const { body: res } = resp;
|
|
if (!res.versions || !Object.keys(res.versions).length) {
|
|
// Registry returned a 200 OK but with no versions
|
|
logger.debug(`No versions returned for npm dependency ${packageName}`);
|
|
return null;
|
|
}
|
|
|
|
const latestVersion = res.versions[res['dist-tags']?.latest ?? ''];
|
|
res.repository ??= latestVersion?.repository;
|
|
res.homepage ??= latestVersion?.homepage;
|
|
|
|
const { sourceUrl, sourceDirectory } = PackageSource.parse(res.repository);
|
|
|
|
// Simplify response before caching and returning
|
|
const dep: ReleaseResult = {
|
|
homepage: res.homepage,
|
|
releases: [],
|
|
tags: res['dist-tags'],
|
|
registryUrl,
|
|
};
|
|
|
|
if (sourceUrl) {
|
|
dep.sourceUrl = sourceUrl;
|
|
}
|
|
|
|
if (sourceDirectory) {
|
|
dep.sourceDirectory = sourceDirectory;
|
|
}
|
|
|
|
if (latestVersion?.deprecated) {
|
|
dep.deprecationMessage = `On registry \`${registryUrl}\`, the "latest" version of dependency \`${packageName}\` has the following deprecation notice:\n\n\`${latestVersion.deprecated}\`\n\nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.`;
|
|
}
|
|
dep.releases = Object.keys(res.versions).map((version) => {
|
|
const release: Release = {
|
|
version,
|
|
gitRef: res.versions?.[version].gitHead,
|
|
dependencies: res.versions?.[version].dependencies,
|
|
devDependencies: res.versions?.[version].devDependencies,
|
|
attestation: isString(res.versions?.[version].dist?.attestations?.url),
|
|
};
|
|
|
|
if (res.versions?.[version].dist?.integrity) {
|
|
release.newDigest = res.versions[version].dist.integrity;
|
|
}
|
|
if (res.versions?.[version].dist?.tarball) {
|
|
release.downloadUrl = res.versions[version].dist.tarball;
|
|
}
|
|
|
|
const releaseTimestamp = asTimestamp(res.time?.[version]);
|
|
if (releaseTimestamp) {
|
|
release.releaseTimestamp = releaseTimestamp;
|
|
}
|
|
if (res.versions?.[version].deprecated) {
|
|
release.isDeprecated = true;
|
|
}
|
|
const nodeConstraint = res.versions?.[version].engines?.node;
|
|
if (isNonEmptyString(nodeConstraint)) {
|
|
release.constraints = { node: [nodeConstraint] };
|
|
}
|
|
const source = PackageSource.parse(res.versions?.[version].repository);
|
|
if (source.sourceUrl && source.sourceUrl !== dep.sourceUrl) {
|
|
release.sourceUrl = source.sourceUrl;
|
|
}
|
|
if (
|
|
source.sourceDirectory &&
|
|
source.sourceDirectory !== dep.sourceDirectory
|
|
) {
|
|
release.sourceDirectory = source.sourceDirectory;
|
|
}
|
|
if (dep.deprecationMessage) {
|
|
release.isDeprecated = true;
|
|
}
|
|
return release;
|
|
});
|
|
|
|
const isPublic = resp.headers?.['cache-control']
|
|
?.toLocaleLowerCase()
|
|
?.split(regEx(/\s*,\s*/))
|
|
?.includes('public');
|
|
if (!isPublic) {
|
|
dep.isPrivate = true;
|
|
}
|
|
|
|
logger.trace({ dep }, 'dep');
|
|
return dep;
|
|
} catch (err) {
|
|
const actualError = err instanceof ExternalHostError ? err.err : err;
|
|
const ignoredStatusCodes = [401, 402, 403, 404];
|
|
const ignoredResponseCodes = ['ENOTFOUND'];
|
|
if (
|
|
actualError.message === HOST_BLOCKED ||
|
|
actualError.message === HOST_DISABLED ||
|
|
ignoredStatusCodes.includes(actualError.statusCode) ||
|
|
ignoredResponseCodes.includes(actualError.code)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (err instanceof ExternalHostError) {
|
|
if (actualError.name === 'ParseError' && actualError.body) {
|
|
actualError.body = 'err.body deleted by Renovate';
|
|
err.err = actualError;
|
|
}
|
|
throw err;
|
|
}
|
|
logger.debug({ err, packageName, registryUrl }, 'Unknown npm lookup error');
|
|
return null;
|
|
}
|
|
}
|