2018-05-16 05:23:59 +00:00
|
|
|
const URL = require('url');
|
2017-08-20 07:49:39 +00:00
|
|
|
const ghGot = require('gh-got');
|
2018-04-09 04:07:05 +00:00
|
|
|
const delay = require('delay');
|
2017-10-17 18:44:40 +00:00
|
|
|
const parseLinkHeader = require('parse-link-header');
|
2019-03-17 06:22:18 +00:00
|
|
|
const pAll = require('p-all');
|
|
|
|
|
2018-09-12 10:16:17 +00:00
|
|
|
const hostRules = require('../../util/host-rules');
|
2019-01-07 05:38:24 +00:00
|
|
|
const { maskToken } = require('../../util/mask');
|
2017-08-20 07:49:39 +00:00
|
|
|
|
2017-11-16 21:13:54 +00:00
|
|
|
let cache = {};
|
2018-09-04 15:56:28 +00:00
|
|
|
let stats = {};
|
2017-11-16 21:13:54 +00:00
|
|
|
|
2019-05-20 08:59:30 +00:00
|
|
|
let endpoint = 'https://api.github.com/';
|
|
|
|
|
2018-07-06 05:26:36 +00:00
|
|
|
async function get(path, options, retries = 5) {
|
2019-05-24 15:40:39 +00:00
|
|
|
let url = URL.resolve(endpoint, path);
|
2018-07-06 05:26:36 +00:00
|
|
|
const opts = {
|
2019-05-10 09:54:01 +00:00
|
|
|
// TODO: Move to configurable host rules, or use utils/got
|
|
|
|
timeout: 60 * 1000,
|
2019-05-24 15:40:39 +00:00
|
|
|
...hostRules.find({ hostType: 'github', url }),
|
2018-07-06 05:26:36 +00:00
|
|
|
...options,
|
|
|
|
};
|
2018-11-09 11:50:21 +00:00
|
|
|
delete opts.endpoint;
|
2018-07-06 05:26:36 +00:00
|
|
|
const method = opts.method || 'get';
|
2019-05-26 19:17:00 +00:00
|
|
|
const useCache = opts.useCache === false ? false : true;
|
2018-09-12 17:58:21 +00:00
|
|
|
if (method.toLowerCase() === 'post' && path === 'graphql') {
|
|
|
|
// GitHub Enterprise uses unversioned graphql path
|
2019-05-20 08:59:30 +00:00
|
|
|
url = url.replace('/v3/', '/');
|
2018-09-12 17:58:21 +00:00
|
|
|
}
|
2018-07-05 07:42:28 +00:00
|
|
|
if (method === 'get' && useCache && cache[path]) {
|
2018-02-06 13:17:46 +00:00
|
|
|
logger.trace({ path }, 'Returning cached result');
|
2017-11-16 21:13:54 +00:00
|
|
|
return cache[path];
|
|
|
|
}
|
2018-09-05 12:18:31 +00:00
|
|
|
logger.trace(`${method.toUpperCase()} ${path}`);
|
2018-09-05 09:03:47 +00:00
|
|
|
stats.requests = (stats.requests || []).concat([
|
2018-09-12 17:17:02 +00:00
|
|
|
method.toUpperCase() + ' ' + path,
|
2018-09-05 09:03:47 +00:00
|
|
|
]);
|
2017-08-20 07:49:39 +00:00
|
|
|
try {
|
2018-09-07 04:28:07 +00:00
|
|
|
if (global.appMode) {
|
2017-10-17 06:04:58 +00:00
|
|
|
const appAccept = 'application/vnd.github.machine-man-preview+json';
|
2017-10-16 07:45:31 +00:00
|
|
|
opts.headers = Object.assign(
|
2017-10-17 06:04:58 +00:00
|
|
|
{},
|
2017-10-16 07:45:31 +00:00
|
|
|
{
|
2017-10-17 06:04:58 +00:00
|
|
|
accept: appAccept,
|
2018-08-28 15:07:00 +00:00
|
|
|
'user-agent':
|
|
|
|
process.env.RENOVATE_USER_AGENT ||
|
|
|
|
'https://github.com/renovatebot/renovate',
|
2017-10-16 07:45:31 +00:00
|
|
|
},
|
|
|
|
opts.headers
|
|
|
|
);
|
2017-10-17 06:04:58 +00:00
|
|
|
if (opts.headers.accept !== appAccept) {
|
|
|
|
opts.headers.accept = `${appAccept}, ${opts.headers.accept}`;
|
|
|
|
}
|
2017-10-16 07:45:31 +00:00
|
|
|
}
|
2019-05-20 08:59:30 +00:00
|
|
|
const res = await ghGot(url, opts);
|
2018-09-04 15:56:28 +00:00
|
|
|
if (res && res.headers) {
|
|
|
|
stats.rateLimit = res.headers['x-ratelimit-limit'];
|
|
|
|
stats.rateLimitRemaining = res.headers['x-ratelimit-remaining'];
|
|
|
|
}
|
2018-04-09 04:07:05 +00:00
|
|
|
if (opts.paginate) {
|
2017-10-17 11:45:17 +00:00
|
|
|
// Check if result is paginated
|
2018-07-03 09:53:09 +00:00
|
|
|
const pageLimit = opts.pageLimit || 10;
|
2017-10-17 18:44:40 +00:00
|
|
|
const linkHeader = parseLinkHeader(res.headers.link);
|
2018-05-16 05:23:59 +00:00
|
|
|
if (linkHeader && linkHeader.next && linkHeader.last) {
|
|
|
|
let lastPage = +linkHeader.last.page;
|
2018-12-19 14:31:20 +00:00
|
|
|
if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') {
|
2018-07-03 09:53:09 +00:00
|
|
|
lastPage = Math.min(pageLimit, lastPage);
|
2018-05-16 05:23:59 +00:00
|
|
|
}
|
|
|
|
const pageNumbers = Array.from(
|
|
|
|
new Array(lastPage),
|
|
|
|
(x, i) => i + 1
|
|
|
|
).slice(1);
|
2019-03-17 06:22:18 +00:00
|
|
|
const queue = pageNumbers.map(page => () => {
|
2019-05-20 08:59:30 +00:00
|
|
|
const nextUrl = URL.parse(linkHeader.next.url, true);
|
|
|
|
delete nextUrl.search;
|
|
|
|
nextUrl.query.page = page;
|
|
|
|
return get(
|
|
|
|
URL.format(nextUrl),
|
|
|
|
{ ...opts, paginate: false },
|
|
|
|
retries
|
|
|
|
);
|
2019-03-17 06:22:18 +00:00
|
|
|
});
|
|
|
|
const pages = await pAll(queue, { concurrency: 5 });
|
2018-03-06 14:54:27 +00:00
|
|
|
res.body = res.body.concat(
|
2018-05-16 05:23:59 +00:00
|
|
|
...pages.filter(Boolean).map(page => page.body)
|
2018-03-06 14:54:27 +00:00
|
|
|
);
|
2017-10-17 08:12:40 +00:00
|
|
|
}
|
|
|
|
}
|
2018-03-30 11:37:06 +00:00
|
|
|
if (
|
|
|
|
method === 'get' &&
|
|
|
|
(path.startsWith('repos/') ||
|
|
|
|
path.startsWith('https://api.github.com/repos/'))
|
|
|
|
) {
|
2017-11-16 21:13:54 +00:00
|
|
|
cache[path] = res;
|
|
|
|
}
|
2018-09-11 09:05:35 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (method === 'POST' && path === 'graphql') {
|
2018-09-11 13:31:59 +00:00
|
|
|
const goodResult = '{"data":{';
|
|
|
|
if (res.body.startsWith(goodResult)) {
|
|
|
|
if (retries === 0) {
|
|
|
|
logger.info('Recovered graphql query');
|
|
|
|
}
|
|
|
|
} else if (retries > 0) {
|
2018-09-11 13:22:05 +00:00
|
|
|
logger.info('Retrying graphql query');
|
2018-09-11 15:44:30 +00:00
|
|
|
opts.body = opts.body.replace('first: 100', 'first: 25');
|
2018-09-11 13:22:05 +00:00
|
|
|
return get(path, opts, 0);
|
|
|
|
}
|
2018-09-11 09:05:35 +00:00
|
|
|
}
|
2017-08-20 07:49:39 +00:00
|
|
|
return res;
|
|
|
|
} catch (err) {
|
2018-09-06 09:30:08 +00:00
|
|
|
if (
|
|
|
|
err.name === 'RequestError' &&
|
|
|
|
(err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')
|
|
|
|
) {
|
2018-09-03 08:58:20 +00:00
|
|
|
throw new Error('platform-failure');
|
|
|
|
}
|
2018-12-03 09:49:07 +00:00
|
|
|
if (err.name === 'ParseError') {
|
|
|
|
throw new Error('platform-failure');
|
|
|
|
}
|
2018-07-25 13:22:23 +00:00
|
|
|
if (err.statusCode >= 500 && err.statusCode < 600) {
|
|
|
|
if (retries > 0) {
|
|
|
|
logger.info(
|
|
|
|
{ statusCode: err.statusCode, message: err.message },
|
|
|
|
`Retrying request`
|
|
|
|
);
|
2018-04-09 04:07:05 +00:00
|
|
|
|
2018-07-25 13:22:23 +00:00
|
|
|
await delay(5000 / retries);
|
2018-04-09 04:07:05 +00:00
|
|
|
|
2018-07-25 13:22:23 +00:00
|
|
|
return get(path, opts, retries - 1);
|
|
|
|
}
|
2019-04-28 20:11:02 +00:00
|
|
|
logger.info({ err }, 'Giving up on GitHub 5xx error');
|
2018-07-25 13:22:23 +00:00
|
|
|
throw new Error('platform-failure');
|
2018-07-09 09:28:33 +00:00
|
|
|
}
|
|
|
|
if (
|
2017-08-27 13:10:19 +00:00
|
|
|
err.statusCode === 403 &&
|
|
|
|
err.message &&
|
2018-06-04 18:44:32 +00:00
|
|
|
err.message.startsWith('You have triggered an abuse detection mechanism')
|
2017-08-27 13:10:19 +00:00
|
|
|
) {
|
2018-10-28 13:46:52 +00:00
|
|
|
if (retries > 0) {
|
|
|
|
logger.info(
|
|
|
|
{
|
|
|
|
headers: err.headers,
|
|
|
|
path,
|
|
|
|
statusCode: err.statusCode,
|
|
|
|
message: err.message,
|
|
|
|
},
|
|
|
|
`Retrying request`
|
|
|
|
);
|
2018-04-09 04:07:05 +00:00
|
|
|
|
2018-10-28 13:46:52 +00:00
|
|
|
await delay(180000 / (retries * retries));
|
2018-04-09 04:07:05 +00:00
|
|
|
|
2018-10-28 13:46:52 +00:00
|
|
|
return get(path, opts, 0);
|
|
|
|
}
|
|
|
|
// istanbul ignore next
|
|
|
|
throw new Error('platform-failure');
|
2018-07-09 09:28:33 +00:00
|
|
|
}
|
2019-01-15 10:15:23 +00:00
|
|
|
if (
|
|
|
|
err.statusCode === 403 &&
|
|
|
|
err.message &&
|
|
|
|
err.message.startsWith(
|
|
|
|
'Upgrade to GitHub Pro or make this repository public to enable this feature'
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
// istanbul ignore next
|
|
|
|
throw err;
|
|
|
|
}
|
2018-07-09 09:28:33 +00:00
|
|
|
if (
|
2017-09-16 04:39:04 +00:00
|
|
|
err.statusCode === 403 &&
|
|
|
|
err.message &&
|
2018-03-22 08:26:20 +00:00
|
|
|
err.message.includes('rate limit exceeded')
|
2017-09-16 04:39:04 +00:00
|
|
|
) {
|
2018-08-28 14:10:33 +00:00
|
|
|
logger.info({ err, headers: err.headers }, 'Rate limit exceeded');
|
2018-03-22 08:26:20 +00:00
|
|
|
throw new Error('rate-limit-exceeded');
|
2018-06-03 07:44:35 +00:00
|
|
|
} else if (
|
|
|
|
err.statusCode === 403 &&
|
|
|
|
err.message &&
|
|
|
|
err.message.includes('blobs up to 1 MB in size')
|
|
|
|
) {
|
|
|
|
throw err;
|
2018-10-16 14:40:13 +00:00
|
|
|
} else if (
|
|
|
|
err.statusCode === 403 &&
|
2018-12-03 11:03:46 +00:00
|
|
|
err.message &&
|
|
|
|
err.message.startsWith('Resource not accessible by integration')
|
2018-10-16 14:40:13 +00:00
|
|
|
) {
|
2019-02-13 15:08:06 +00:00
|
|
|
logger.debug({ err }, 'Unauthorized');
|
2018-12-03 11:03:46 +00:00
|
|
|
throw new Error('integration-unauthorized');
|
|
|
|
} else if (err.statusCode === 403) {
|
2017-10-11 13:18:50 +00:00
|
|
|
if (retries > 0) {
|
|
|
|
logger.info(
|
|
|
|
{ statusCode: err.statusCode, message: err.message },
|
|
|
|
`Retrying request`
|
|
|
|
);
|
2018-04-09 04:07:05 +00:00
|
|
|
|
|
|
|
await delay(60000 / (retries * retries));
|
|
|
|
|
2017-10-17 05:15:01 +00:00
|
|
|
return get(path, opts, retries - 1);
|
2017-09-16 04:39:04 +00:00
|
|
|
}
|
2017-10-20 04:24:09 +00:00
|
|
|
} else if (
|
|
|
|
err.statusCode === 401 &&
|
|
|
|
err.message &&
|
2018-05-16 04:01:02 +00:00
|
|
|
err.message.includes('Bad credentials')
|
2017-10-20 04:24:09 +00:00
|
|
|
) {
|
2018-07-21 06:38:13 +00:00
|
|
|
const rateLimit = err.headers ? err.headers['x-ratelimit-limit'] : -1;
|
2018-06-19 17:18:01 +00:00
|
|
|
logger.info(
|
2018-05-16 04:01:02 +00:00
|
|
|
{
|
2018-09-04 12:51:58 +00:00
|
|
|
token: maskToken(opts.token),
|
|
|
|
err,
|
2018-05-16 04:01:02 +00:00
|
|
|
},
|
|
|
|
'Bad credentials'
|
|
|
|
);
|
2018-07-23 11:11:56 +00:00
|
|
|
if (rateLimit === '60') {
|
2018-07-21 06:38:13 +00:00
|
|
|
throw new Error('platform-failure');
|
|
|
|
}
|
2018-05-16 04:01:02 +00:00
|
|
|
throw new Error('bad-credentials');
|
2017-09-16 04:39:04 +00:00
|
|
|
}
|
2017-08-20 07:49:39 +00:00
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete'];
|
|
|
|
|
|
|
|
for (const x of helpers) {
|
2017-12-14 10:47:00 +00:00
|
|
|
get[x] = (url, opts) =>
|
|
|
|
get(url, Object.assign({}, opts, { method: x.toUpperCase() }));
|
2017-08-20 07:49:39 +00:00
|
|
|
}
|
|
|
|
|
2018-09-07 04:28:07 +00:00
|
|
|
get.setAppMode = function setAppMode() {
|
|
|
|
// no-op
|
2017-10-16 07:45:31 +00:00
|
|
|
};
|
|
|
|
|
2017-12-14 10:47:00 +00:00
|
|
|
get.reset = function reset() {
|
2018-04-04 12:16:36 +00:00
|
|
|
cache = null;
|
2017-11-16 21:13:54 +00:00
|
|
|
cache = {};
|
2018-09-04 15:56:28 +00:00
|
|
|
// istanbul ignore if
|
2018-09-05 12:19:03 +00:00
|
|
|
if (stats.requests && stats.requests.length > 1) {
|
2018-09-05 09:03:47 +00:00
|
|
|
logger.info(
|
|
|
|
{
|
2018-09-11 07:57:11 +00:00
|
|
|
rateLimit: parseInt(stats.rateLimit, 10),
|
2018-09-05 09:03:47 +00:00
|
|
|
requestCount: stats.requests.length,
|
2018-09-11 07:57:11 +00:00
|
|
|
rateLimitRemaining: parseInt(stats.rateLimitRemaining, 10),
|
2018-09-05 09:03:47 +00:00
|
|
|
},
|
|
|
|
'Request stats'
|
|
|
|
);
|
|
|
|
stats.requests.sort();
|
|
|
|
logger.debug({ requests: stats.requests }, 'All requests');
|
2018-09-05 12:19:03 +00:00
|
|
|
stats = {};
|
2018-09-04 15:56:28 +00:00
|
|
|
}
|
2017-11-16 21:13:54 +00:00
|
|
|
};
|
|
|
|
|
2019-05-20 08:59:30 +00:00
|
|
|
get.setEndpoint = e => {
|
|
|
|
endpoint = e;
|
|
|
|
};
|
|
|
|
|
2017-10-17 05:15:01 +00:00
|
|
|
module.exports = get;
|