mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-26 08:04:36 +00:00
1cb84a79df
## Problem The issue-lifecycle stale rule checked labels with case-sensitive `Array.includes()`, so an issue carrying the `Stale` label (created by the `actions/stale` PR bot) was never recognized as stale. `hasStaleLabel` stayed `false` and the bot re-posted the 7-day warning on **every** daily cron run. Example: [#3441](https://github.com/openfrontio/OpenFrontIO/issues/3441) got the same "hasn't had activity in 7 days" comment ~16 days in a row. ## Fix GitHub label names are case-insensitive (you can't have both `Stale` and `stale`), so the gate should be too. Adds a `hasLabel()` helper in `github.ts` and routes all label checks through it (`STALE`, `KEEP_OPEN`, `APPROVED`, `NOT_APPROVED`). Now an issue gets one stale warning when marked, then silence until the 14-day close. ## Note The Dependabot PR-exemption change (`pr-stale.yml`) is being applied separately — the CI token here lacks `workflow` scope to push workflow-file changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
import type { Octokit } from "@octokit/rest";
|
|
import { COMMENTS, LABELS, STALE_CLOSE_DAYS, STALE_WARN_DAYS } from "../config";
|
|
import {
|
|
type Action,
|
|
type Issue,
|
|
type IssueComment,
|
|
hasLabel,
|
|
isBotUser,
|
|
listIssueComments,
|
|
} from "../github";
|
|
|
|
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
|
|
export function daysBetween(fromIso: string, now: Date): number {
|
|
const from = new Date(fromIso).getTime();
|
|
return (now.getTime() - from) / MS_PER_DAY;
|
|
}
|
|
|
|
export function latestNonBotActivityIso(
|
|
issue: Issue,
|
|
comments: IssueComment[],
|
|
): string {
|
|
let latest = issue.created_at;
|
|
for (const c of comments) {
|
|
if (isBotUser(c.user)) continue;
|
|
if (new Date(c.created_at).getTime() > new Date(latest).getTime()) {
|
|
latest = c.created_at;
|
|
}
|
|
}
|
|
return latest;
|
|
}
|
|
|
|
export async function checkStale(
|
|
issue: Issue,
|
|
octokit: Octokit,
|
|
now: Date = new Date(),
|
|
): Promise<Action[]> {
|
|
if (issue.milestone !== null) return [];
|
|
if (hasLabel(issue, LABELS.KEEP_OPEN)) return [];
|
|
|
|
const comments = await listIssueComments(octokit, issue.number);
|
|
const lastActivityIso = latestNonBotActivityIso(issue, comments);
|
|
const daysSinceActivity = daysBetween(lastActivityIso, now);
|
|
const hasStaleLabel = hasLabel(issue, LABELS.STALE);
|
|
const authorLogin = issue.user?.login ?? "there";
|
|
|
|
if (hasStaleLabel && daysSinceActivity < STALE_WARN_DAYS) {
|
|
return [{ type: "remove_label", label: LABELS.STALE }];
|
|
}
|
|
if (hasStaleLabel && daysSinceActivity >= STALE_CLOSE_DAYS) {
|
|
return [
|
|
{ type: "add_label", label: LABELS.AUTO_CLOSED_STALE },
|
|
{ type: "comment", body: COMMENTS.AUTO_CLOSED_STALE(authorLogin) },
|
|
{ type: "close", reason: "not_planned" },
|
|
];
|
|
}
|
|
if (!hasStaleLabel && daysSinceActivity >= STALE_WARN_DAYS) {
|
|
return [
|
|
{ type: "add_label", label: LABELS.STALE },
|
|
{ type: "comment", body: COMMENTS.STALE_WARNING(authorLogin) },
|
|
];
|
|
}
|
|
return [];
|
|
}
|