fix(util/host-rules): resolve enabled per trust tier

As part of follow-up changes, we're looking to tighten the way that
Renovate handles outgoing HTTP traffic, and a self-hosted administrator
disabling a host needs to be something a repository cannot undo.

In a similar change to
4732da7d2b, we can resolve `enabled` per
trust tier, so that a repository or preset rule can no longer re-enable
- or disable - a host the administrator's own rules set, which was
previously possible with a longer `matchHost`.

Within a tier nothing changes: the most specific rule still wins, so a
repository disabling a host of its own keeps working, as does an
administrator masking their own broad rule with a narrower one.

This is a behaviour change for repository config that previously relied
on overriding an admin rule's `enabled`.

Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
This commit is contained in:
Jamie Tanna
2026-09-07 15:32:42 +01:00
committed by Jamie Tanna
parent 0db7cdd91f
commit 7d3fe44060
3 changed files with 61 additions and 0 deletions
+4
View File
@@ -2182,6 +2182,10 @@ To match specific ports you have to add a protocol to `matchHost`:
Disabling a host is only 100% effective if added to self-hosted config.
Renovate currently still checks its _cache_ for results first before trying to connect, so if a public host is blocked in your repository config (e.g. `renovate.json`) then it's possible you may get cached _results_ from that host if another repository using the same Renovate deployment has successfully queried for the same dependency recently.
!!! note
`enabled` is not resolved by specificity alone: host rules from the self-hosted administrator's own config are resolved at a higher trust level than those from repository config or a preset.
So if the administrator's own rules set `enabled` for a host, repository config or a preset cannot re-enable - or disable - that host, no matter how specific its `matchHost` is.
### `hostRules.abortIgnoreStatusCodes`
This field can be used to configure status codes that Renovate ignores and passes through when `abortOnError` is set to `true`.
+39
View File
@@ -690,6 +690,45 @@ describe('util/host-rules', () => {
});
});
describe('enabled trust tiers', () => {
it('does not let a more specific repository rule re-enable a host the admin disabled', () => {
add(
{ matchHost: 'bad.example.com', enabled: false },
{ trusted: true },
);
add({ matchHost: 'https://bad.example.com/deeper', enabled: true });
expect(
find({ url: 'https://bad.example.com/deeper/path' }).enabled,
).toBeFalse();
});
it('does not let a repository rule disable a host the admin enabled', () => {
add(
{ matchHost: 'good.example.com', enabled: true },
{ trusted: true },
);
add({ matchHost: 'https://good.example.com/deeper', enabled: false });
expect(
find({ url: 'https://good.example.com/deeper/path' }).enabled,
).toBeTrue();
});
it('keeps repository rules able to disable a host of their own', () => {
add({ matchHost: 'flaky.example.com', enabled: false });
expect(find({ url: 'https://flaky.example.com' }).enabled).toBeFalse();
});
it('lets the most specific rule of a tier win', () => {
add({ matchHost: 'example.com', enabled: false });
add({ matchHost: 'https://ok.example.com', enabled: true });
expect(find({ url: 'https://ok.example.com' }).enabled).toBeTrue();
});
});
it('prefers the longest matchHost for a header they both set', () => {
add({
matchHost: 'https://registry.example.com',
+18
View File
@@ -258,6 +258,13 @@ function headersOfLastRuleToSetThem(
.pop();
}
/**
* The last defined value, following the same "most specific rule wins" ordering as {@link headersOfLastRuleToSetThem}.
*/
function lastDefined<T>(values: (T | undefined)[]): T | undefined {
return values.filter((value) => !isUndefined(value)).pop();
}
export function find(search: HostRuleSearch): CombinedHostRule {
if ([search.hostType, search.url].every(isFalsy)) {
logger.warn({ search }, 'Invalid hostRules search');
@@ -318,6 +325,17 @@ export function find(search: HostRuleSearch): CombinedHostRule {
res.headers = { ...untrustedHeaders, ...trustedHeaders };
}
// `enabled` is resolved per trust tier like `headers`: a repository or preset rule must not be able to re-enable a host the administrator's own rules disabled (or vice versa) by out-specifying them with a longer `matchHost`
const trustedRules = matchedRules.filter((rule) => rule.trusted);
const untrustedEnabled = lastDefined(
matchedRules.filter((rule) => !rule.trusted).map((rule) => rule.enabled),
);
const trustedEnabled = lastDefined(trustedRules.map((rule) => rule.enabled));
const enabled = trustedEnabled ?? untrustedEnabled;
if (!isUndefined(enabled)) {
res.enabled = enabled;
}
delete res.hostType;
delete res.resolvedHost;
delete res.matchHost;