mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
16ef1715e9
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules Flag ad-hoc type checks that have @sindresorhus/is equivalents: - .filter(Boolean) -> .filter(isTruthy) - typeof x === 'string' -> isString(x) - x === null || x === undefined -> isNullOrUndefined(x) (and negation) The typeof x === 'object' shape lives in a separate suggestion-only rule (renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject() are not drop-in equivalents: null, arrays and functions behave differently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: fix renovate/prefer-is-helpers violations Replace ad-hoc type checks with @sindresorhus/is helpers across lib/: - 13x .filter(Boolean) -> .filter(isTruthy) - 36x typeof x ==/!== 'string' -> isString(x) / !isString(x) - 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation) Promote renovate/prefer-is-helpers to error now that the codebase is clean. renovate/prefer-is-object stays at warn: its 15 remaining sites guard parsed YAML/JSON or unknown values where isObject()/isPlainObject() are not provably equivalent (null/array/function semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: enforce renovate/prefer-is-object at error level Resolve the 15 remaining warnings and raise the rule from warn to error: - Migrate 10 sites to isObject() where the guarded value provably cannot be a function (parsed YAML/JSON, typed exec command arguments, or test-controlled fixtures): helm-values, woodpecker, bun, pre-commit managers, exec utils, file package cache, and the github http spec. In the bun manager this also fixes a latent crash where a null `workspaces` value would throw on the `in` operator. - Keep 5 sites as reasoned oxlint-disable comments where excluding functions is load-bearing: the byte-exact fingerprint canonicalizer (2), the circularity walker, the zod result guard, and the generic hasKey helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
237 lines
6.1 KiB
TypeScript
237 lines
6.1 KiB
TypeScript
import { isFalsy, isString, isTruthy, isUndefined } from '@sindresorhus/is';
|
|
import { logger } from '../logger/index.ts';
|
|
import type { CombinedHostRule, HostRule } from '../types/index.ts';
|
|
import { clone } from './clone.ts';
|
|
import * as sanitize from './sanitize.ts';
|
|
import { toBase64 } from './string.ts';
|
|
import { isHttpUrl, massageHostUrl, parseUrl } from './url.ts';
|
|
|
|
let hostRules: HostRule[] = [];
|
|
|
|
/**
|
|
* Fields within `HostRule`s that must have their value registered for sanitising through `sanitize.addSecretForSanitizing()`.
|
|
*
|
|
* Kept in sync with `redactedFields` through tests.
|
|
*/
|
|
export const confidentialFields: (keyof HostRule)[] = [
|
|
'password',
|
|
'token',
|
|
'httpsPrivateKey',
|
|
/* not actually sensitive, but redacted nonetheless */
|
|
'httpsCertificate',
|
|
/* not actually sensitive, but redacted nonetheless */
|
|
'httpsCertificateAuthority',
|
|
];
|
|
|
|
export interface LegacyHostRule {
|
|
hostName?: string;
|
|
domainName?: string;
|
|
baseUrl?: string;
|
|
host?: string;
|
|
endpoint?: string;
|
|
}
|
|
|
|
export function migrateRule(rule: LegacyHostRule & HostRule): HostRule {
|
|
const cloned: LegacyHostRule & HostRule = clone(rule);
|
|
delete cloned.hostName;
|
|
delete cloned.domainName;
|
|
delete cloned.baseUrl;
|
|
const result: HostRule = cloned;
|
|
|
|
const { matchHost } = result;
|
|
const { hostName, domainName, baseUrl } = rule;
|
|
const hostValues = [matchHost, hostName, domainName, baseUrl].filter(
|
|
isTruthy,
|
|
);
|
|
if (hostValues.length === 1) {
|
|
const [matchHost] = hostValues;
|
|
result.matchHost = matchHost;
|
|
} else if (hostValues.length > 1) {
|
|
throw new Error(
|
|
`hostRules cannot contain more than one host-matching field - use "matchHost" only.`,
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function add(params: HostRule): void {
|
|
const rule = migrateRule(params);
|
|
|
|
if (rule.matchHost) {
|
|
rule.matchHost = massageHostUrl(rule.matchHost);
|
|
const parsedUrl = parseUrl(rule.matchHost);
|
|
rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;
|
|
confidentialFields.forEach((field) => {
|
|
if (rule[field]) {
|
|
logger.debug(
|
|
// TODO: types (#22198)
|
|
`Adding ${field} authentication for ${rule.matchHost!} (hostType=${
|
|
rule.hostType
|
|
}) to hostRules`,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
confidentialFields.forEach((field) => {
|
|
const secret = rule[field];
|
|
if (isString(secret) && secret.length > 3) {
|
|
sanitize.addSecretForSanitizing(secret);
|
|
}
|
|
});
|
|
if (rule.username && rule.password) {
|
|
sanitize.addSecretForSanitizing(
|
|
toBase64(`${rule.username}:${rule.password}`),
|
|
);
|
|
}
|
|
hostRules.push(rule);
|
|
}
|
|
|
|
export interface HostRuleSearch {
|
|
hostType?: string;
|
|
url?: string;
|
|
readOnly?: boolean;
|
|
}
|
|
|
|
export function matchesHost(url: string, matchHost: string): boolean {
|
|
const parsedUrl = parseUrl(url);
|
|
if (!parsedUrl) {
|
|
return false;
|
|
}
|
|
|
|
const parsedMatchHost = parseUrl(matchHost);
|
|
if (isHttpUrl(parsedUrl) && isHttpUrl(parsedMatchHost)) {
|
|
return parsedUrl.href.startsWith(parsedMatchHost!.href);
|
|
}
|
|
|
|
const { hostname } = parsedUrl;
|
|
if (!hostname) {
|
|
return false;
|
|
}
|
|
|
|
if (hostname === matchHost) {
|
|
return true;
|
|
}
|
|
|
|
const topLevelSuffix = matchHost.startsWith('.')
|
|
? matchHost
|
|
: `.${matchHost}`;
|
|
return hostname.endsWith(topLevelSuffix);
|
|
}
|
|
|
|
function fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {
|
|
if (!a.matchHost || !b.matchHost) {
|
|
return 0;
|
|
}
|
|
return a.matchHost.length - b.matchHost.length;
|
|
}
|
|
|
|
function hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {
|
|
if ((hostType || readOnly) && matchHost) {
|
|
return 3;
|
|
}
|
|
|
|
if (matchHost) {
|
|
return 2;
|
|
}
|
|
|
|
if (hostType) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function fromLowerToHigherRank(a: HostRule, b: HostRule): number {
|
|
return hostRuleRank(a) - hostRuleRank(b);
|
|
}
|
|
|
|
export function find(search: HostRuleSearch): CombinedHostRule {
|
|
if ([search.hostType, search.url].every(isFalsy)) {
|
|
logger.warn({ search }, 'Invalid hostRules search');
|
|
return {};
|
|
}
|
|
|
|
// Sort primarily by rank, and secondarily by matchHost length
|
|
const sortedRules = hostRules
|
|
.sort(fromShorterToLongerMatchHost)
|
|
.sort(fromLowerToHigherRank);
|
|
|
|
const matchedRules: HostRule[] = [];
|
|
for (const rule of sortedRules) {
|
|
let hostTypeMatch = true;
|
|
let hostMatch = true;
|
|
let readOnlyMatch = true;
|
|
|
|
if (rule.hostType) {
|
|
hostTypeMatch = false;
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (search.hostType === rule.hostType) {
|
|
hostTypeMatch = true;
|
|
}
|
|
}
|
|
|
|
if (rule.matchHost && rule.resolvedHost) {
|
|
hostMatch = false;
|
|
if (search.url) {
|
|
hostMatch = matchesHost(search.url, rule.matchHost);
|
|
}
|
|
}
|
|
|
|
if (!isUndefined(rule.readOnly)) {
|
|
readOnlyMatch = false;
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (search.readOnly === rule.readOnly) {
|
|
readOnlyMatch = true;
|
|
hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`
|
|
}
|
|
}
|
|
|
|
if (hostTypeMatch && readOnlyMatch && hostMatch) {
|
|
matchedRules.push(clone(rule));
|
|
}
|
|
}
|
|
|
|
const res: HostRule = Object.assign({}, ...matchedRules);
|
|
delete res.hostType;
|
|
delete res.resolvedHost;
|
|
delete res.matchHost;
|
|
delete res.readOnly;
|
|
return res;
|
|
}
|
|
|
|
export function hosts({ hostType }: { hostType: string }): string[] {
|
|
return hostRules
|
|
.filter((rule) => rule.hostType === hostType)
|
|
.map((rule) => rule.resolvedHost)
|
|
.filter(isTruthy);
|
|
}
|
|
|
|
export function hostType({ url }: { url: string }): string | null {
|
|
return (
|
|
hostRules
|
|
.filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost))
|
|
.sort(fromShorterToLongerMatchHost)
|
|
.map((rule) => rule.hostType)
|
|
.filter(isTruthy)
|
|
.pop() ?? null
|
|
);
|
|
}
|
|
|
|
export function findAll({ hostType }: { hostType: string }): HostRule[] {
|
|
return hostRules.filter((rule) => rule.hostType === hostType);
|
|
}
|
|
|
|
/**
|
|
* @returns a deep copy of all known host rules without any filtering
|
|
*/
|
|
export function getAll(): HostRule[] {
|
|
return clone(hostRules);
|
|
}
|
|
|
|
export function clear(): void {
|
|
logger.debug('Clearing hostRules');
|
|
hostRules = [];
|
|
sanitize.clearRepoSanitizedSecretsList();
|
|
}
|