Skip to content

GIthubAction to close stale PRs #3061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions .github/workflows/StalePRs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# A workflow copied from the pytorch/pytorch repo stale PRs 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 at midnight UTC.
- cron: '0 0 * * *'
workflow_dispatch:

jobs:
stale:
if: ${{ github.repository == 'pytorch/tutorials' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- uses: actions/github-script@v6
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`. <br>" +
"Feel free to remove the `Stale` label if you feel this was a mistake. <br>" +
"If you are unable to remove the `Stale` label please contact a maintainer in order to do so. <br>" +
"If you want the bot to never mark this PR stale again, add the `no-stale` label.<br>" +
"`Stale` pull requests will automatically be closed after 30 days of inactivity.<br>";

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: "tutorials",
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: "tutorials",
issue_number: pull.number,
body: STALE_MESSAGE,
});

numAPIRequests += 1;
await github.rest.issues.addLabels({
owner: "pytorch",
repo: "tutorials",
issue_number: pull.number,
labels: ["Stale"],
});
}
}

for await (const response of github.paginate.iterator(
github.rest.pulls.list,
{
owner: "pytorch",
repo: "tutorials",
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.`);

Loading