Files
ProxmoxVE/.github/workflows/stale_pr_close.yml
T
CanbiZ (MickLesk) bffc871aa9 Flag instead of closing on first PR template miss; add script-request eligibility check (#18384)
close-invalid-pr-template.yml now comments and labels (missing pr template,
stale) instead of closing immediately, clears itself once fixed. stale_pr_close.yml
gains the engagement-gated/keep-open/maintainer-reset behavior already used in
DevScripts, so a template-stale PR only closes via its own re-check.

request-script.yml discussion template gets an optional Justification field;
a new workflow verifies stars/age against GitHub for the submitted repo and
either does nothing (eligible), labels investigation (ineligible + justified),
or closes the discussion (ineligible, no justification).

Moved the AI Assistance section to the end of pull_request_template.md.
2026-11-03 14:58:57 +01:00

134 lines
6.3 KiB
YAML
Generated

name: Stale PR Management
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
pull_request_target:
types:
- labeled
jobs:
stale-prs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
steps:
- name: Handle stale PRs
uses: actions/github-script@v9
with:
script: |
const now = new Date();
const owner = context.repo.owner;
const repo = context.repo.repo;
const TEMPLATE_LABEL = "missing pr template";
const MAINTAINERS = ["OWNER", "MEMBER", "COLLABORATOR"];
const days = d => (now - d) / (1000 * 60 * 60 * 24);
const staleNotice = author =>
`@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`;
// Only fires for a maintainer's label: labels set with GITHUB_TOKEN raise no event.
if (context.eventName === "pull_request_target" && context.payload.action === "labeled") {
const pr = context.payload.pull_request;
const labels = (pr.labels || []).map(l => l.name);
if (context.payload.label?.name === "stale" && !labels.includes(TEMPLATE_LABEL)) {
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number, body: staleNotice(pr.user.login)
});
}
return;
}
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
for (const pr of prs) {
const labels = pr.labels.map(l => l.name);
const author = pr.user.login;
const hasStale = labels.includes("stale");
if (labels.includes("keep-open")) {
if (hasStale) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
}
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100
});
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner, repo, pull_number: pr.number, per_page: 100
});
const lastCommitDate = commits.length > 0
? new Date(commits[commits.length - 1].commit.author.date)
: new Date(pr.created_at);
const humanComments = comments.filter(c => c.user?.type !== "Bot");
// -------------------------------------------------------
// Auto-label PRs with no activity in the last 14 days,
// but ONLY if someone other than the author has engaged.
// -------------------------------------------------------
if (!hasStale) {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner, repo, pull_number: pr.number, per_page: 100
});
const engaged =
reviews.some(r => r.user?.type !== "Bot" && r.user?.login !== author) ||
humanComments.some(c => c.user?.login !== author);
if (!engaged) continue;
const lastCommentDate = humanComments.length > 0
? new Date(humanComments[humanComments.length - 1].created_at)
: null;
const lastActivityDate = lastCommentDate && lastCommentDate > lastCommitDate
? lastCommentDate
: lastCommitDate;
if (days(lastActivityDate) > 14) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ["stale"] });
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: staleNotice(author) });
}
continue;
}
// -------------------------------------------------------
// Manage already-stale PRs
// -------------------------------------------------------
const events = await github.paginate(github.rest.issues.listEvents, {
owner, repo, issue_number: pr.number, per_page: 100
});
const staleDates = events
.filter(e => e.event === "labeled" && e.label?.name === "stale")
.map(e => new Date(e.created_at));
if (staleDates.length === 0) continue;
const staleLabelDate = new Date(Math.max(...staleDates));
const isTemplateIssue = labels.includes(TEMPLATE_LABEL);
// A push ends an inactivity mark; a template mark only ends via close-invalid-pr-template.yml's re-check.
if (!isTemplateIssue && lastCommitDate > staleLabelDate) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: `@${author} Recent activity detected. Removing stale label.`
});
continue;
}
// A maintainer comment after the mark restarts the 7 days.
const maintainerDates = humanComments
.filter(c => MAINTAINERS.includes(c.author_association) && c.user?.login !== author)
.map(c => new Date(c.created_at))
.filter(d => d > staleLabelDate);
const clockStart = maintainerDates.length > 0 ? new Date(Math.max(...maintainerDates)) : staleLabelDate;
if (days(clockStart) <= 7) continue;
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: isTemplateIssue
? `@${author} Closing: the PR template requirements listed above were still missing 7 days later. Once they are in place, please open a new pull request.`
: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
});
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: "closed" });
}