Skip to content

Commit

Permalink
feat: abortOnError / abortIgnoreStatusCodes (#6556)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Jun 23, 2020
1 parent 9219709 commit daf2a48
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 10 deletions.
61 changes: 61 additions & 0 deletions docs/usage/configuration-options.md
Expand Up @@ -445,6 +445,67 @@ Example for configuring `docker` auth:
}
```

### abortIgnoreStatusCodes

This field can be used to configure status codes that Renovate ignores and passes through when `abortOnError` is set to `true`. For example to also skip 404 responses then configure the following:

```json
{
"hostRules": [
{
"abortOnError": true,
"abortStatusCodes": [404]
}
]
}
```

Note that this field is _not_ mergeable, so the last-applied host rule will take precedence.

### 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 _not_ matching any of the configured `abortIgnoreStatusCodes` (e.g. `500 Internal Error`);

### 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
34 changes: 31 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,28 @@ 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: 'abortIgnoreStatusCodes',
description:
'A list of HTTP status codes to ignore and *not* abort the run because of when abortOnError=true.',
type: 'array',
subType: 'number',
stage: 'repository',
parent: 'hostRules',
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;
abortIgnoreStatusCodes?: 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', 'abortIgnoreStatusCodes', 'timeout'].forEach((param) => {
if (foundRules[param]) {
options[param] = foundRules[param];
}
});
return options;
}
26 changes: 26 additions & 0 deletions lib/util/http/index.spec.ts
@@ -1,5 +1,7 @@
import nock from 'nock';
import { getName } from '../../../test/util';
import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages';
import * as hostRules from '../host-rules';
import { Http } from '.';

const baseUrl = 'http://renovate.com';
Expand All @@ -10,12 +12,36 @@ 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 404 error to ExternalHostError', async () => {
nock(baseUrl).get('/test').reply(404);
hostRules.add({ abortOnError: true });
await expect(http.get('http://renovate.com/test')).rejects.toThrow(
EXTERNAL_HOST_ERROR
);
expect(nock.isDone()).toBe(true);
});
it('ignores 404 error and does not throw ExternalHostError', async () => {
nock(baseUrl).get('/test').reply(404);
hostRules.add({ abortOnError: true, abortIgnoreStatusCodes: [404] });
await expect(http.get('http://renovate.com/test')).rejects.toThrow(
'Response code 404 (Not Found)'
);
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
22 changes: 19 additions & 3 deletions lib/util/http/index.ts
@@ -1,6 +1,7 @@
import crypto from 'crypto';
import URL from 'url';
import got from 'got';
import { ExternalHostError } from '../../types/error';
import * as runCache from '../cache/run';
import { clone } from '../clone';
import { applyAuthorization } from './auth';
Expand Down Expand Up @@ -41,6 +42,21 @@ function cloneResponse<T>(response: any): HttpResponse<T> {
};
}

async function resolveResponse<T>(
promisedRes: Promise<HttpResponse<T>>,
{ abortOnError, abortIgnoreStatusCodes }
): Promise<HttpResponse<T>> {
try {
const res = await promisedRes;
return cloneResponse(res);
} catch (err) {
if (abortOnError && !abortIgnoreStatusCodes?.includes(err.statusCode)) {
throw new ExternalHostError(err);
}
throw err;
}
}

export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
constructor(private hostType: string, private options?: HttpOptions) {}

Expand Down Expand Up @@ -102,23 +118,23 @@ export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> {
const cachedRes = runCache.get(cacheKey);
// istanbul ignore if
if (cachedRes) {
return cloneResponse<T>(await cachedRes);
return resolveResponse<T>(cachedRes, options);
}
}
const startTime = Date.now();
const promisedRes = got(url, options);
if (options.method === 'get') {
runCache.set(cacheKey, promisedRes); // always set if it's a get
}
const res = await promisedRes;
const res = await resolveResponse<T>(promisedRes, options);
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);
return res;
}

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

0 comments on commit daf2a48

Please sign in to comment.