# A workflow that implements similar logic to actions/stale. # # Compared to actions/stale, it is implemented to make API requests proportional # to the number of stale PRs, not the total number of issues in the repo. This # is because PyTorch has a lot of issues/PRs, so the actions/stale runs into # rate limits way too quickly. # # The behavior is: # - If a PR is not labeled stale, after 60 days inactivity label the PR as stale and comment about it. # - If a PR is labeled stale, after 30 days inactivity close the PR. # - `high priority` and `no-stale` PRs are exempt. name: Close stale pull requests on: schedule: # Run hourly. - cron: 30 * * * * workflow_dispatch: jobs: stale: if: ${{ github.repository == 'pytorch/pytorch' }} runs-on: linux.large permissions: contents: read pull-requests: write steps: - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | // Do some dumb retries on requests. const retries = 7; const baseBackoff = 100; const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout)); github.hook.wrap('request', async (request, options) => { for (let attempt = 1; attempt <= retries; attempt++) { try { return await request(options); } catch (err) { if (attempt < retries) { core.warning(`Request getting retried. Attempt: ${attempt}`); await sleep(baseBackoff * Math.pow(2, attempt)); continue; } throw err; } } }); const MAX_API_REQUESTS = 100; // If a PRs not labeled stale, label them stale after no update for 60 days. const STALE_LABEL_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 60; // For PRs already labeled stale, close after not update for 30 days. const STALE_CLOSE_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 30; const STALE_MESSAGE = "Looks like this PR hasn't been updated in a while so we're going to go ahead and mark this as `Stale`.
" + "Feel free to remove the `Stale` label if you feel this was a mistake.
" + "If you are unable to remove the `Stale` label please contact a maintainer in order to do so.
" + "If you want the bot to never mark this PR stale again, add the `no-stale` label.
" + "`Stale` pull requests will automatically be closed after 30 days of inactivity.
"; let numAPIRequests = 0; let numProcessed = 0; async function processPull(pull) { core.info(`[${pull.number}] URL: ${pull.html_url}`); numProcessed += 1; const labels = pull.labels.map((label) => label.name); // Skip if certain labels are present. if (labels.includes("no-stale") || labels.includes("high priority")) { core.info(`[${pull.number}] Skipping because PR has an exempting label.`); return false; } // Check if the PR is stale, according to our configured thresholds. let staleThresholdMillis; if (labels.includes("Stale")) { core.info(`[${pull.number}] PR is labeled stale, checking whether we should close it.`); staleThresholdMillis = STALE_CLOSE_THRESHOLD_MS; } else { core.info(`[${pull.number}] Checking whether to label PR as stale.`); staleThresholdMillis = STALE_LABEL_THRESHOLD_MS; } const millisSinceLastUpdated = new Date().getTime() - new Date(pull.updated_at).getTime(); if (millisSinceLastUpdated < staleThresholdMillis) { core.info(`[${pull.number}] Skipping because PR was updated recently`); return false; } // At this point, we know we should do something. // For PRs already labeled stale, close them. if (labels.includes("Stale")) { core.info(`[${pull.number}] Closing PR.`); numAPIRequests += 1; await github.rest.issues.update({ owner: "pytorch", repo: "pytorch", issue_number: pull.number, state: "closed", }); } else { // For PRs not labeled stale, label them stale. core.info(`[${pull.number}] Labeling PR as stale.`); numAPIRequests += 1; await github.rest.issues.createComment({ owner: "pytorch", repo: "pytorch", issue_number: pull.number, body: STALE_MESSAGE, }); numAPIRequests += 1; await github.rest.issues.addLabels({ owner: "pytorch", repo: "pytorch", issue_number: pull.number, labels: ["Stale"], }); } } for await (const response of github.paginate.iterator( github.rest.pulls.list, { owner: "pytorch", repo: "pytorch", state: "open", sort: "created", direction: "asc", per_page: 100, } )) { numAPIRequests += 1; const pulls = response.data; // Awaiting in a loop is intentional here. We want to serialize execution so // that log groups are printed correctl for (const pull of pulls) { if (numAPIRequests > MAX_API_REQUESTS) { core.warning("Max API requests exceeded, exiting."); process.exit(0); } await core.group(`Processing PR #${pull.number}`, async () => { await processPull(pull); }); } } core.info(`Processed ${numProcessed} PRs total.`);