mirror of
https://github.com/zebrajr/pytorch.git
synced 2026-01-15 12:15:51 +00:00
Pin actions from repos external to the PyTorch project to their shasums for security. This is a best practice as Git tags are not immutable. https://openssf.org/blog/2024/08/12/mitigating-attack-vectors-in-github-workflows/ Pull Request resolved: https://github.com/pytorch/pytorch/pull/152110 Approved by: https://github.com/seemethere, https://github.com/ZainRizvi
157 lines
6.2 KiB
YAML
157 lines
6.2 KiB
YAML
# 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`. <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: "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.`);
|