1module.exports = async ({ github, core }, callback) => {
2 const Bottleneck = require('bottleneck')
3
4 const stats = {
5 issues: 0,
6 prs: 0,
7 requests: 0,
8 artifacts: 0,
9 }
10
11 // Rate-Limiting and Throttling, see for details:
12 // https://github.com/octokit/octokit.js/issues/1069#throttling
13 // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api
14 const allLimits = new Bottleneck({
15 // Avoid concurrent requests
16 maxConcurrent: 1,
17 // Will be updated with first `updateReservoir()` call below.
18 reservoir: 0,
19 })
20 // Pause between mutative requests
21 const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits)
22 github.hook.wrap('request', async (request, options) => {
23 // Requests to a different host do not count against the rate limit.
24 if (options.url.startsWith('https://github.com')) return request(options)
25 // Requests to the /rate_limit endpoint do not count against the rate limit.
26 if (options.url === '/rate_limit') return request(options)
27 // Search requests are in a different resource group, which allows 30 requests / minute.
28 // We do less than a handful each run, so not implementing throttling for now.
29 if (options.url.startsWith('/search/')) return request(options)
30 stats.requests++
31 if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method))
32 return writeLimits.schedule(request.bind(null, options))
33 else return allLimits.schedule(request.bind(null, options))
34 })
35
36 async function updateReservoir() {
37 let response
38 try {
39 response = await github.rest.rateLimit.get()
40 } catch (err) {
41 core.error(`Failed updating reservoir:\n${err}`)
42 // Keep retrying on failed rate limit requests instead of exiting the script early.
43 return
44 }
45 // Always keep 1000 spare requests for other jobs to do their regular duty.
46 // They normally use below 100, so 1000 is *plenty* of room to work with.
47 const reservoir = Math.max(0, response.data.resources.core.remaining - 1000)
48 core.info(`Updating reservoir to: ${reservoir}`)
49 allLimits.updateSettings({ reservoir })
50 }
51 await updateReservoir()
52 // Update remaining requests every minute to account for other jobs running in parallel.
53 const reservoirUpdater = setInterval(updateReservoir, 60 * 1000)
54
55 try {
56 await callback(stats)
57 } finally {
58 clearInterval(reservoirUpdater)
59 core.notice(
60 `Processed ${stats.prs} PRs, ${stats.issues} Issues, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`,
61 )
62 }
63}