Skip to content

Commit a9a33dd

Browse files
authored
feat(presets): fetch presets from HTTP URLs (#27359)
1 parent bdc8e67 commit a9a33dd

File tree

5 files changed

+158
-0
lines changed

5 files changed

+158
-0
lines changed

docs/usage/config-presets.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Presets can be nested.
3333

3434
Presets should be hosted in repositories, which usually means the same platform host as Renovate is running against.
3535

36+
Alternatively, Renovate can fetch preset files from an HTTP server.
37+
3638
<!-- prettier-ignore -->
3739
!!! warning
3840
We deprecated npm-based presets.
@@ -208,6 +210,28 @@ This is especially helpful in self-hosted scenarios where public presets cannot
208210
Local presets are specified either by leaving out any prefix, e.g. `owner/name`, or explicitly by adding a `local>` prefix, e.g. `local>owner/name`.
209211
Renovate will determine the current platform and look up the preset from there.
210212

213+
## Fetching presets from an HTTP server
214+
215+
If your desired platform is not yet supported, or if you want presets to work when you run Renovate with `--platform=local`, you can specify presets using HTTP URLs:
216+
217+
```json
218+
{
219+
"extends": [
220+
"http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain"
221+
]
222+
}
223+
```
224+
225+
Parameters are supported similar to other methods:
226+
227+
```json
228+
{
229+
"extends": [
230+
"http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain(param)"
231+
]
232+
}
233+
```
234+
211235
## Contributing to presets
212236

213237
Have you configured a rule that could help others?
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as httpMock from '../../../../test/http-mock';
2+
import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util';
3+
import * as http from '.';
4+
5+
const host = 'https://my.server/';
6+
const filePath = '/test-preset.json';
7+
const repo = 'https://my.server/test-preset.json';
8+
9+
describe('config/presets/http/index', () => {
10+
describe('getPreset()', () => {
11+
it('should return parsed JSON', async () => {
12+
httpMock.scope(host).get(filePath).reply(200, { foo: 'bar' });
13+
14+
expect(await http.getPreset({ repo })).toEqual({ foo: 'bar' });
15+
});
16+
17+
it('should return parsed JSON5', async () => {
18+
httpMock
19+
.scope('https://my.server/')
20+
.get('/test-preset.json5')
21+
.reply(200, '{ foo: "bar" } // comment');
22+
23+
const repo = 'https://my.server/test-preset.json5';
24+
25+
expect(await http.getPreset({ repo })).toEqual({ foo: 'bar' });
26+
});
27+
28+
it('throws if fails to parse', async () => {
29+
httpMock.scope(host).get(filePath).reply(200, 'not json');
30+
31+
await expect(http.getPreset({ repo })).rejects.toThrow(
32+
PRESET_INVALID_JSON,
33+
);
34+
});
35+
36+
it('throws if file not found', async () => {
37+
httpMock.scope(host).get(filePath).reply(404);
38+
39+
await expect(http.getPreset({ repo })).rejects.toThrow(
40+
PRESET_DEP_NOT_FOUND,
41+
);
42+
});
43+
44+
it('throws on malformed URL', async () => {
45+
await expect(http.getPreset({ repo: 'malformed!' })).rejects.toThrow(
46+
PRESET_DEP_NOT_FOUND,
47+
);
48+
});
49+
});
50+
});

lib/config/presets/http/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { logger } from '../../../logger';
2+
import { ExternalHostError } from '../../../types/errors/external-host-error';
3+
import { Http } from '../../../util/http';
4+
import type { HttpResponse } from '../../../util/http/types';
5+
import { parseUrl } from '../../../util/url';
6+
import type { Preset, PresetConfig } from '../types';
7+
import { PRESET_DEP_NOT_FOUND, parsePreset } from '../util';
8+
9+
const http = new Http('preset');
10+
11+
export async function getPreset({
12+
repo: url,
13+
}: PresetConfig): Promise<Preset | null | undefined> {
14+
const parsedUrl = parseUrl(url);
15+
let response: HttpResponse;
16+
17+
if (!parsedUrl) {
18+
logger.debug(`Preset URL ${url} is malformed`);
19+
throw new Error(PRESET_DEP_NOT_FOUND);
20+
}
21+
22+
try {
23+
response = await http.get(url);
24+
} catch (err) {
25+
// istanbul ignore if: not testable with nock
26+
if (err instanceof ExternalHostError) {
27+
throw err;
28+
}
29+
30+
logger.debug(`Preset file ${url} not found`);
31+
throw new Error(PRESET_DEP_NOT_FOUND);
32+
}
33+
34+
return parsePreset(response.body, parsedUrl.pathname);
35+
}

lib/config/presets/index.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,48 @@ describe('config/presets/index', () => {
936936
presetSource: 'npm',
937937
});
938938
});
939+
940+
it('parses HTTPS URLs', () => {
941+
expect(
942+
presets.parsePreset(
943+
'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
944+
),
945+
).toEqual({
946+
repo: 'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
947+
params: undefined,
948+
presetName: '',
949+
presetPath: undefined,
950+
presetSource: 'http',
951+
});
952+
});
953+
954+
it('parses HTTP URLs', () => {
955+
expect(
956+
presets.parsePreset(
957+
'http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain',
958+
),
959+
).toEqual({
960+
repo: 'http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain',
961+
params: undefined,
962+
presetName: '',
963+
presetPath: undefined,
964+
presetSource: 'http',
965+
});
966+
});
967+
968+
it('parses HTTPS URLs with parameters', () => {
969+
expect(
970+
presets.parsePreset(
971+
'https://my.server/gitea/renovate-config/raw/branch/main/default.json(param1)',
972+
),
973+
).toEqual({
974+
repo: 'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
975+
params: ['param1'],
976+
presetName: '',
977+
presetPath: undefined,
978+
presetSource: 'http',
979+
});
980+
});
939981
});
940982

941983
describe('getPreset', () => {

lib/config/presets/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { removedPresets } from './common';
1919
import * as gitea from './gitea';
2020
import * as github from './github';
2121
import * as gitlab from './gitlab';
22+
import * as http from './http';
2223
import * as internal from './internal';
2324
import * as local from './local';
2425
import * as npm from './npm';
@@ -39,6 +40,7 @@ const presetSources: Record<string, PresetApi> = {
3940
gitea,
4041
local,
4142
internal,
43+
http,
4244
};
4345

4446
const presetCacheNamespace = 'preset';
@@ -122,6 +124,8 @@ export function parsePreset(input: string): ParsedPreset {
122124
} else if (str.startsWith('local>')) {
123125
presetSource = 'local';
124126
str = str.substring('local>'.length);
127+
} else if (str.startsWith('http://') || str.startsWith('https://')) {
128+
presetSource = 'http';
125129
} else if (
126130
!str.startsWith('@') &&
127131
!str.startsWith(':') &&
@@ -138,6 +142,9 @@ export function parsePreset(input: string): ParsedPreset {
138142
.map((elem) => elem.trim());
139143
str = str.slice(0, str.indexOf('('));
140144
}
145+
if (presetSource === 'http') {
146+
return { presetSource, repo: str, presetName: '', params };
147+
}
141148
const presetsPackages = [
142149
'compatibility',
143150
'config',

0 commit comments

Comments
 (0)