diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 9f8427124821..2ae77cf01421 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -39,7 +39,12 @@ jobs: if: github.event_name != 'schedule' || github.repository_owner == 'NixOS' steps: - name: Install dependencies - run: npm install @actions/artifact + run: npm install @actions/artifact bottleneck + + - name: Log current API rate limits + env: + GH_TOKEN: ${{ github.token }} + run: gh api /rate_limit | jq - name: Labels from API data and Eval results uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 @@ -47,12 +52,41 @@ jobs: UPDATED_WITHIN: ${{ inputs.updatedWithin }} with: script: | + const Bottleneck = require('bottleneck') const path = require('node:path') const { DefaultArtifactClient } = require('@actions/artifact') const { readFile } = require('node:fs/promises') const artifactClient = new DefaultArtifactClient() + const stats = { + prs: 0, + requests: 0, + artifacts: 0 + } + + // Rate-Limiting and Throttling, see for details: + // https://github.com/octokit/octokit.js/issues/1069#throttling + // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api + const allLimits = new Bottleneck({ + // Avoid concurrent requests + maxConcurrent: 1, + // Hourly limit is at 5000, but other jobs need some, too! + // https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api + reservoir: 1000, + reservoirRefreshAmount: 1000, + reservoirRefreshInterval: 60 * 60 * 1000 + }) + // Pause between mutative requests + const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits) + github.hook.wrap('request', async (request, options) => { + stats.requests++ + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) + return writeLimits.schedule(request.bind(null, options)) + else + return allLimits.schedule(request.bind(null, options)) + }) + if (process.env.UPDATED_WITHIN && !/^\d+$/.test(process.env.UPDATED_WITHIN)) throw new Error('Please enter "updated within" as integer in hours.') @@ -90,7 +124,7 @@ jobs: base: context.payload.pull_request.base.ref } - await github.paginate( + const prs = await github.paginate( github.rest.pulls.list, { ...context.repo, @@ -99,12 +133,16 @@ jobs: direction: 'desc', ...prEventCondition }, - async (response, done) => (await Promise.allSettled(response.data.map(async (pull_request) => { + (response, done) => response.data.map(async (pull_request) => { try { - const log = (k,v) => core.info(`PR #${pull_request.number} - ${k}: ${v}`) + const log = (k,v,skip) => { + core.info(`PR #${pull_request.number} - ${k}: ${v}` + (skip ? ' (skipped)' : '')) + return skip + } - log('Last updated at', pull_request.updated_at) - if (new Date(pull_request.updated_at) < cutoff) return done() + if (log('Last updated at', pull_request.updated_at, new Date(pull_request.updated_at) < cutoff)) + return done() + stats.prs++ log('URL', pull_request.html_url) const run_id = (await github.rest.actions.listWorkflowRuns({ @@ -119,8 +157,8 @@ jobs: // Newer PRs might not have run Eval to completion, yet. We can skip them, because this // job will be run as part of that Eval run anyway. - log('Last eval run', run_id ?? '') - if (!run_id) return; + if (log('Last eval run', run_id ?? '', !run_id)) + return; const artifact = (await github.rest.actions.listWorkflowRunArtifacts({ ...context.repo, @@ -132,8 +170,10 @@ jobs: // actually download the artifact in the next step and avoid that race condition. // Older PRs, where the workflow run was already eval.yml, but the artifact was not // called "comparison", yet, will be skipped as well. - log('Artifact expires at', artifact?.expires_at ?? '') - if (new Date(artifact?.expires_at ?? 0) < new Date(new Date().getTime() + 60 * 1000)) return; + const expired = new Date(artifact?.expires_at ?? 0) < new Date(new Date().getTime() + 60 * 1000) + if (log('Artifact expires at', artifact?.expires_at ?? '', expired)) + return; + stats.artifacts++ await artifactClient.downloadArtifact(artifact.id, { findBy: { @@ -195,10 +235,14 @@ jobs: } catch (cause) { throw new Error(`Labeling PR #${pull_request.number} failed.`, { cause }) } - }))) + }) + ); + + (await Promise.allSettled(prs.flat())) .filter(({ status }) => status == 'rejected') .map(({ reason }) => core.setFailed(`${reason.message}\n${reason.cause.stack}`)) - ) + + core.notice(`Processed ${stats.prs} PRs, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`) - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 name: Labels from touched files