Skip to content

Commit

Permalink
feat(presets): fetch presets from HTTP URLs (#27359)
Browse files Browse the repository at this point in the history
  • Loading branch information
amezin committed Feb 24, 2024
1 parent bdc8e67 commit a9a33dd
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 0 deletions.
24 changes: 24 additions & 0 deletions docs/usage/config-presets.md
Expand Up @@ -33,6 +33,8 @@ Presets can be nested.

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

Alternatively, Renovate can fetch preset files from an HTTP server.

<!-- prettier-ignore -->
!!! warning
We deprecated npm-based presets.
Expand Down Expand Up @@ -208,6 +210,28 @@ This is especially helpful in self-hosted scenarios where public presets cannot
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`.
Renovate will determine the current platform and look up the preset from there.

## Fetching presets from an HTTP server

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:

```json
{
"extends": [
"http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain"
]
}
```

Parameters are supported similar to other methods:

```json
{
"extends": [
"http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain(param)"
]
}
```

## Contributing to presets

Have you configured a rule that could help others?
Expand Down
50 changes: 50 additions & 0 deletions lib/config/presets/http/index.spec.ts
@@ -0,0 +1,50 @@
import * as httpMock from '../../../../test/http-mock';
import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util';
import * as http from '.';

const host = 'https://my.server/';
const filePath = '/test-preset.json';
const repo = 'https://my.server/test-preset.json';

describe('config/presets/http/index', () => {
describe('getPreset()', () => {
it('should return parsed JSON', async () => {
httpMock.scope(host).get(filePath).reply(200, { foo: 'bar' });

expect(await http.getPreset({ repo })).toEqual({ foo: 'bar' });
});

it('should return parsed JSON5', async () => {
httpMock
.scope('https://my.server/')
.get('/test-preset.json5')
.reply(200, '{ foo: "bar" } // comment');

const repo = 'https://my.server/test-preset.json5';

expect(await http.getPreset({ repo })).toEqual({ foo: 'bar' });
});

it('throws if fails to parse', async () => {
httpMock.scope(host).get(filePath).reply(200, 'not json');

await expect(http.getPreset({ repo })).rejects.toThrow(
PRESET_INVALID_JSON,
);
});

it('throws if file not found', async () => {
httpMock.scope(host).get(filePath).reply(404);

await expect(http.getPreset({ repo })).rejects.toThrow(
PRESET_DEP_NOT_FOUND,
);
});

it('throws on malformed URL', async () => {
await expect(http.getPreset({ repo: 'malformed!' })).rejects.toThrow(
PRESET_DEP_NOT_FOUND,
);
});
});
});
35 changes: 35 additions & 0 deletions lib/config/presets/http/index.ts
@@ -0,0 +1,35 @@
import { logger } from '../../../logger';
import { ExternalHostError } from '../../../types/errors/external-host-error';
import { Http } from '../../../util/http';
import type { HttpResponse } from '../../../util/http/types';
import { parseUrl } from '../../../util/url';
import type { Preset, PresetConfig } from '../types';
import { PRESET_DEP_NOT_FOUND, parsePreset } from '../util';

const http = new Http('preset');

export async function getPreset({
repo: url,
}: PresetConfig): Promise<Preset | null | undefined> {
const parsedUrl = parseUrl(url);
let response: HttpResponse;

if (!parsedUrl) {
logger.debug(`Preset URL ${url} is malformed`);
throw new Error(PRESET_DEP_NOT_FOUND);
}

try {
response = await http.get(url);
} catch (err) {
// istanbul ignore if: not testable with nock
if (err instanceof ExternalHostError) {
throw err;
}

logger.debug(`Preset file ${url} not found`);
throw new Error(PRESET_DEP_NOT_FOUND);
}

return parsePreset(response.body, parsedUrl.pathname);
}
42 changes: 42 additions & 0 deletions lib/config/presets/index.spec.ts
Expand Up @@ -936,6 +936,48 @@ describe('config/presets/index', () => {
presetSource: 'npm',
});
});

it('parses HTTPS URLs', () => {
expect(
presets.parsePreset(
'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
),
).toEqual({
repo: 'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
params: undefined,
presetName: '',
presetPath: undefined,
presetSource: 'http',
});
});

it('parses HTTP URLs', () => {
expect(
presets.parsePreset(
'http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain',
),
).toEqual({
repo: 'http://my.server/users/me/repos/renovate-presets/raw/default.json?at=refs%2Fheads%2Fmain',
params: undefined,
presetName: '',
presetPath: undefined,
presetSource: 'http',
});
});

it('parses HTTPS URLs with parameters', () => {
expect(
presets.parsePreset(
'https://my.server/gitea/renovate-config/raw/branch/main/default.json(param1)',
),
).toEqual({
repo: 'https://my.server/gitea/renovate-config/raw/branch/main/default.json',
params: ['param1'],
presetName: '',
presetPath: undefined,
presetSource: 'http',
});
});
});

describe('getPreset', () => {
Expand Down
7 changes: 7 additions & 0 deletions lib/config/presets/index.ts
Expand Up @@ -19,6 +19,7 @@ import { removedPresets } from './common';
import * as gitea from './gitea';
import * as github from './github';
import * as gitlab from './gitlab';
import * as http from './http';
import * as internal from './internal';
import * as local from './local';
import * as npm from './npm';
Expand All @@ -39,6 +40,7 @@ const presetSources: Record<string, PresetApi> = {
gitea,
local,
internal,
http,
};

const presetCacheNamespace = 'preset';
Expand Down Expand Up @@ -122,6 +124,8 @@ export function parsePreset(input: string): ParsedPreset {
} else if (str.startsWith('local>')) {
presetSource = 'local';
str = str.substring('local>'.length);
} else if (str.startsWith('http://') || str.startsWith('https://')) {
presetSource = 'http';
} else if (
!str.startsWith('@') &&
!str.startsWith(':') &&
Expand All @@ -138,6 +142,9 @@ export function parsePreset(input: string): ParsedPreset {
.map((elem) => elem.trim());
str = str.slice(0, str.indexOf('('));
}
if (presetSource === 'http') {
return { presetSource, repo: str, presetName: '', params };
}
const presetsPackages = [
'compatibility',
'config',
Expand Down

0 comments on commit a9a33dd

Please sign in to comment.