Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: abortOnError / abortIgnoreStatusCodes #6556

Merged
merged 11 commits into from Jun 23, 2020
73 changes: 73 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -445,6 +445,79 @@ Example for configuring `docker` auth:
}
```

### abortOnError

Use this field to configure Renovate to abort runs for custom hosts. By default, Renovate will only abort for known public hosts, which has the downside that transient errors for other hosts can cause autoclosing of PRs.

To abort Renovate runs for http failures from _any_ host:

```json
{
"hostRules": [
{
"abortOnError": true
}
]
}
```

To abort Renovate runs for any `docker` datasource failures:

```json
{
"hostRules": [
{
"hostType": "docker",
"abortOnError": true
}
]
}
```

To abort Renovate for errors for a specific `docker` host:

```json
{
"hostRules": [
{
"hostName": "docker.company.com",
"abortOnError": true
}
]
}
```

When this field is enabled, Renovate will abort its run if it encounters either (a) any low-level http error (e.g. `ETIMEDOUT`) or (b) receives a response matching any of the configured `abortStatusCodes` (e.g. `500 Internal Error`);

### abortStatusCodes

This field can be used to override the default status codes that Renovate decides should cause an abort. For example to also abort for 401 and 403 responses then configure the following:

```json
{
"hostRules": [
{
"abortStatusCodes": [
401,
403,
408,
413,
429,
500,
502,
503,
504,
521,
522,
524
]
}
]
}
```

Note that this field is _not_ mergeable, so if you want to add any status codes to the defaults then you need to include the full list. However, this also means you can make the default list smaller, if needed.

### baseUrl

Use this instead of `domainName` or `hostName` if you need a rule to apply to a specific path on a host. For example, `"baseUrl": "https://api.github.com"` is equivalent to `"hostName": "api.github.com"` but `"baseUrl": "https://api.github.com/google/"` is not.
Expand Down
35 changes: 32 additions & 3 deletions lib/config/definitions.ts
Expand Up @@ -38,19 +38,24 @@ export interface RenovateOptionBase {
stage?: RenovateConfigStage;
}

export interface RenovateArrayOption<T extends string | object = object>
extends RenovateOptionBase {
export interface RenovateArrayOption<
T extends string | number | object = object
> extends RenovateOptionBase {
default?: T[];
mergeable?: boolean;
type: 'array';
subType?: 'string' | 'object';
subType?: 'string' | 'object' | 'number';
}

export interface RenovateStringArrayOption extends RenovateArrayOption<string> {
format?: 'regex';
subType: 'string';
}

export interface RenovateNumberArrayOption extends RenovateArrayOption<number> {
subType: 'number';
}

export interface RenovateBooleanOption extends RenovateOptionBase {
default?: boolean;
type: 'boolean';
Expand Down Expand Up @@ -79,6 +84,7 @@ export interface RenovateObjectOption extends RenovateOptionBase {

export type RenovateOptions =
| RenovateStringOption
| RenovateNumberArrayOption
| RenovateStringArrayOption
| RenovateIntegerOption
| RenovateBooleanOption
Expand Down Expand Up @@ -1613,6 +1619,29 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'abortOnError',
description:
'If enabled, Renovate will abort its run when http request errors occur.',
type: 'boolean',
stage: 'repository',
parent: 'hostRules',
default: false,
cli: false,
env: false,
},
{
name: 'abortStatusCodes',
description:
'A list of HTTP status codes to trigger a run abort if received.',
type: 'array',
subType: 'number',
stage: 'repository',
parent: 'hostRules',
default: [408, 413, 429, 500, 502, 503, 504, 521, 522, 524],
cli: false,
env: false,
},
{
name: 'prBodyDefinitions',
description: 'Table column definitions for use in PR tables',
Expand Down
2 changes: 2 additions & 0 deletions lib/types/host-rules.ts
Expand Up @@ -13,4 +13,6 @@ export interface HostRule {
platform?: string;
timeout?: number;
encrypted?: HostRule;
abortOnError?: boolean;
abortStatusCodes?: number[];
}
11 changes: 7 additions & 4 deletions lib/util/http/host-rules.ts
Expand Up @@ -10,7 +10,7 @@ export function applyHostRules(url: string, inOptions: any): any {
hostType: options.hostType,
url,
}) || {};
const { username, password, token, timeout } = foundRules;
const { username, password, token } = foundRules;
if (options.headers?.authorization || options.auth || options.token) {
logger.trace('Authorization already set for host: ' + options.hostname);
} else if (password) {
Expand All @@ -20,8 +20,11 @@ export function applyHostRules(url: string, inOptions: any): any {
logger.trace('Applying Bearer authentication for host ' + options.hostname);
options.token = token;
}
if (timeout) {
options.timeout = timeout;
}
// Apply optional params
['abortOnError', 'abortStatusCodes', 'timeout'].forEach((param) => {
if (foundRules[param]) {
options[param] = foundRules[param];
}
});
return options;
}
19 changes: 19 additions & 0 deletions lib/util/http/index.spec.ts
@@ -1,5 +1,7 @@
import nock from 'nock';
import { getName } from '../../../test/util';
import { DATASOURCE_FAILURE } from '../../constants/error-messages';
import * as hostRules from '../host-rules';
import { Http } from '.';

const baseUrl = 'http://renovate.com';
Expand All @@ -10,12 +12,29 @@ describe(getName(__filename), () => {
beforeEach(() => {
http = new Http('dummy');
nock.cleanAll();
hostRules.clear();
});
it('get', async () => {
nock(baseUrl).get('/test').reply(200);
expect(await http.get('http://renovate.com/test')).toMatchSnapshot();
expect(nock.isDone()).toBe(true);
});
it('returns 429 error', async () => {
nock(baseUrl).get('/test').reply(429);
await expect(http.get('http://renovate.com/test')).rejects.toThrow(
'Response code 429 (Too Many Requests)'
);
expect(nock.isDone()).toBe(true);
});
it('converts 429 error to DatasourceError', async () => {
nock(baseUrl).get('/test').reply(429);
// TODO: set abortStatusCodes default value
hostRules.add({ abortOnError: true, abortStatusCodes: [429] });
await expect(http.get('http://renovate.com/test')).rejects.toThrow(
DATASOURCE_FAILURE
);
expect(nock.isDone()).toBe(true);
});
it('getJson', async () => {
nock(baseUrl).get('/').reply(200, '{ "test": true }');
expect(await http.getJson('http://renovate.com')).toMatchSnapshot();
Expand Down
30 changes: 21 additions & 9 deletions lib/util/http/index.ts
@@ -1,6 +1,7 @@
import crypto from 'crypto';
import URL from 'url';
import got from 'got';
import { DatasourceError } from '../../datasource';
import * as runCache from '../cache/run';
import { clone } from '../clone';
import { applyAuthorization } from './auth';
Expand Down Expand Up @@ -110,15 +111,26 @@ export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
if (options.method === 'get') {
runCache.set(cacheKey, promisedRes); // always set if it's a get
}
const res = await promisedRes;
const httpRequests = runCache.get('http-requests') || [];
httpRequests.push({
method: options.method,
url,
duration: Date.now() - startTime,
});
runCache.set('http-requests', httpRequests);
return cloneResponse<T>(res);
// TODO: make this try/catch shared with the earlier cachedRes
try {
const res = await promisedRes;
const httpRequests = runCache.get('http-requests') || [];
httpRequests.push({
method: options.method,
url,
duration: Date.now() - startTime,
});
runCache.set('http-requests', httpRequests);
return cloneResponse<T>(res);
} catch (err) {
if (
options.abortOnError &&
options.abortStatusCodes?.includes(err.statusCode)
) {
throw new DatasourceError(err);
}
throw err;
}
}

get(url: string, options: HttpOptions = {}): Promise<HttpResponse> {
Expand Down