mirror of
https://github.com/HDFGroup/hdf5.git
synced 2026-09-25 04:09:44 +03:00
* ci: replace actions/stale with bot-aware mark-stale script actions/stale uses updatedAt to measure inactivity, so any bot event (e.g. the /remove-reviewer acknowledgment comment) resets the stale countdown even when there has been no meaningful human activity for months. PR #6332 was last touched by a human on 2026-04-02 but was not flagged because a reviewer-removal on 2026-06-11 refreshed the timestamp. Replace the actions/stale step with a custom mark-stale.js script (same pattern as alert-stale.js) that only counts non-bot comments, non-bot review submissions, and commits as meaningful activity. The script also removes the stale label if such activity occurs after the label was applied. * ci: fix draft-stale keepalive for external contributors Two bugs with the keepalive checkbox on draft-stale PRs: 1. External contributors (fork authors) lack write access to edit the bot's comment, so clicking the checkbox silently fails for them. Fix: also treat a new non-bot comment posted after the keepalive comment as a sufficient keepalive signal. 2. The stale label could take up to 24 hours to be removed (daily cron only). Fix: add an issue_comment.created trigger so draft-pr-policy fires immediately when someone comments on a stale draft PR. mark-stale and alert-stale are guarded to only run on schedule/workflow_dispatch, not on every comment. Also fix lastRealActivityAt to exclude bot comments (matching mark-stale.js), so the keepalive and "Thanks for confirming" bot comments don't count as real activity when measuring staleness.
126 lines
3.8 KiB
JavaScript
126 lines
3.8 KiB
JavaScript
// Marks open, non-draft PRs as stale based on last *meaningful* activity:
|
|
// non-bot comments, review submissions, or commits pushed to the branch.
|
|
// Bot-only events (e.g. /remove-reviewer acknowledgment comments, CI status
|
|
// posts) do not reset the stale countdown.
|
|
const DAYS_BEFORE_STALE = 30;
|
|
const STALE_LABEL = "stale";
|
|
const EXEMPT_LABELS = ["pinned", "security"];
|
|
const STALE_MESSAGE =
|
|
"This pull request has had no activity for 30 days and has been marked stale. " +
|
|
"Push a commit or comment to keep it open, or it will be flagged for maintainer review.";
|
|
|
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
const daysSince = (iso) => (Date.now() - new Date(iso).getTime()) / MS_PER_DAY;
|
|
|
|
async function ensureLabel(github, owner, repo, name) {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name });
|
|
} catch (e) {
|
|
if (e.status !== 404) throw e;
|
|
await github.rest.issues.createLabel({
|
|
owner,
|
|
repo,
|
|
name,
|
|
color: "ededed",
|
|
description: "No meaningful activity for 30+ days",
|
|
});
|
|
}
|
|
}
|
|
|
|
async function lastMeaningfulActivity(github, owner, repo, pr) {
|
|
let latest = new Date(pr.created_at);
|
|
|
|
// Comments from non-bot users only
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
per_page: 100,
|
|
});
|
|
for (const c of comments) {
|
|
if (c.user?.type === "Bot") continue;
|
|
const d = new Date(c.created_at);
|
|
if (d > latest) latest = d;
|
|
}
|
|
|
|
// Reviews from non-bot users only
|
|
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
per_page: 100,
|
|
});
|
|
for (const r of reviews) {
|
|
if (r.user?.type === "Bot") continue;
|
|
const d = new Date(r.submitted_at);
|
|
if (d > latest) latest = d;
|
|
}
|
|
|
|
// Commits pushed to the branch
|
|
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
per_page: 100,
|
|
});
|
|
for (const c of commits) {
|
|
const d = new Date(c.commit.committer?.date || c.commit.author?.date);
|
|
if (d > latest) latest = d;
|
|
}
|
|
|
|
return latest;
|
|
}
|
|
|
|
async function runMarkStale({ github, context, core }) {
|
|
const { owner, repo } = context.repo;
|
|
await ensureLabel(github, owner, repo, STALE_LABEL);
|
|
|
|
const prs = await github.paginate(github.rest.pulls.list, {
|
|
owner,
|
|
repo,
|
|
state: "open",
|
|
per_page: 100,
|
|
});
|
|
|
|
let marked = 0,
|
|
unStaled = 0;
|
|
for (const pr of prs) {
|
|
if (pr.draft) continue;
|
|
|
|
const labelNames = pr.labels.map((l) => (typeof l === "string" ? l : l.name));
|
|
const exempt = EXEMPT_LABELS.some((l) => labelNames.includes(l));
|
|
const alreadyStale = labelNames.includes(STALE_LABEL);
|
|
|
|
const lastActive = await lastMeaningfulActivity(github, owner, repo, pr);
|
|
const days = daysSince(lastActive.toISOString());
|
|
|
|
if (alreadyStale && days < DAYS_BEFORE_STALE) {
|
|
// Meaningful human activity after stale was applied — remove the label
|
|
await github.rest.issues
|
|
.removeLabel({ owner, repo, issue_number: pr.number, name: STALE_LABEL })
|
|
.catch(() => {});
|
|
core.info(`Un-staled PR #${pr.number} (last meaningful activity ${Math.floor(days)} days ago)`);
|
|
unStaled++;
|
|
} else if (!alreadyStale && !exempt && days >= DAYS_BEFORE_STALE) {
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
labels: [STALE_LABEL],
|
|
});
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
body: STALE_MESSAGE,
|
|
});
|
|
core.info(`Marked PR #${pr.number} as stale (${Math.floor(days)} days since last meaningful activity)`);
|
|
marked++;
|
|
}
|
|
}
|
|
|
|
core.info(`mark-stale: marked=${marked} un-staled=${unStaled}`);
|
|
}
|
|
|
|
module.exports = { runMarkStale };
|