diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ae7cbe6857b..e64015fda53 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,12 +3,12 @@ # - 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 +name: Close stale issues and pull requests on: schedule: - # Run daily at 00:30 UTC. - - cron: '30 0 * * *' + # Run weekly at 00:30 UTC every Sunday. + - cron: '30 0 * * 0' workflow_dispatch: jobs: @@ -17,13 +17,13 @@ jobs: runs-on: linux.large permissions: contents: read + issues: write 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)); @@ -43,107 +43,112 @@ jobs: }); 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`.
" + + "Looks like this item 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.
"; + "If you want the bot to never mark this item stale again, add the `no-stale` label.
" + + "`Stale` items 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}`); + async function processItem(item, isPR) { + core.info(`[${item.number}] URL: ${item.html_url}`); numProcessed += 1; - const labels = pull.labels.map((label) => label.name); + const labels = item.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.`); + core.info(`[${item.number}] Skipping because item 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.`); + core.info(`[${item.number}] Item 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.`); + core.info(`[${item.number}] Checking whether to label item as stale.`); staleThresholdMillis = STALE_LABEL_THRESHOLD_MS; } const millisSinceLastUpdated = - new Date().getTime() - new Date(pull.updated_at).getTime(); + new Date().getTime() - new Date(item.updated_at).getTime(); if (millisSinceLastUpdated < staleThresholdMillis) { - core.info(`[${pull.number}] Skipping because PR was updated recently`); + core.info(`[${item.number}] Skipping because item 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.`); + core.info(`[${item.number}] Closing item.`); numAPIRequests += 1; - await github.rest.issues.update({ + await github.rest.issues.update({ owner: "pytorch", repo: "executorch", - issue_number: pull.number, + issue_number: item.number, state: "closed", - }); + }); } else { - // For PRs not labeled stale, label them stale. - core.info(`[${pull.number}] Labeling PR as stale.`); - + core.info(`[${item.number}] Labeling item as stale.`); numAPIRequests += 1; - await github.rest.issues.createComment({ + await github.rest.issues.createComment({ owner: "pytorch", repo: "executorch", - issue_number: pull.number, + issue_number: item.number, body: STALE_MESSAGE, }); numAPIRequests += 1; - await github.rest.issues.addLabels({ + await github.rest.issues.addLabels({ owner: "pytorch", repo: "executorch", - issue_number: pull.number, + issue_number: item.number, labels: ["Stale"], }); } } - for await (const response of github.paginate.iterator( - github.rest.pulls.list, - { - owner: "pytorch", - repo: "executorch", - 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); + async function processType(listFn, isPR) { + for await (const response of github.paginate.iterator( + listFn, + { + owner: "pytorch", + repo: "executorch", + state: "open", + sort: "created", + direction: "asc", + per_page: 100, + } + )) { + numAPIRequests += 1; + const items = response.data; + for (const item of items) { + if (numAPIRequests > MAX_API_REQUESTS) { + core.warning("Max API requests exceeded, exiting."); + process.exit(0); + } + await core.group(`Processing ${isPR ? "PR" : "Issue"} #${item.number}`, async () => { + await processItem(item, isPR); + }); } - await core.group(`Processing PR #${pull.number}`, async () => { - await processPull(pull); - }); } } - core.info(`Processed ${numProcessed} PRs total.`); + + // Process PRs + await processType(github.rest.pulls.list, true); + + // Process Issues (exclude PRs) + await processType( + async (params) => { + const resp = await github.rest.issues.listForRepo(params); + resp.data = resp.data.filter((issue) => !issue.pull_request); + return resp; + }, + false + ); + + core.info(`Processed ${numProcessed} items total.`);