Fix stale bot re-commenting on case-mismatched labels (#4394)

## 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>
This commit is contained in:
Evan
2026-06-23 15:31:27 -07:00
committed by GitHub
parent a852fa22f2
commit 1cb84a79df
3 changed files with 14 additions and 5 deletions
+8
View File
@@ -28,6 +28,14 @@ export function makeOctokit(token: string): Octokit {
return new Octokit({ auth: token });
}
// GitHub label names are case-insensitive (you cannot have both "Stale" and
// "stale"), so match them that way — otherwise a label applied with different
// casing (e.g. the "Stale" label from the PR stale action) is missed.
export function hasLabel(issue: Issue, label: string): boolean {
const target = label.toLowerCase();
return issue.labels.some((l) => l.toLowerCase() === target);
}
export function isBotUser(
user: { login: string; type: string } | null,
): boolean {
@@ -1,9 +1,9 @@
import { LABELS } from "../config";
import type { Action, Issue } from "../github";
import { type Action, hasLabel, type Issue } from "../github";
export function syncApprovalLabel(issue: Issue): Action[] {
const hasApproved = issue.labels.includes(LABELS.APPROVED);
const hasNotApproved = issue.labels.includes(LABELS.NOT_APPROVED);
const hasApproved = hasLabel(issue, LABELS.APPROVED);
const hasNotApproved = hasLabel(issue, LABELS.NOT_APPROVED);
const milestoned = issue.milestone !== null;
const actions: Action[] = [];
@@ -4,6 +4,7 @@ import {
type Action,
type Issue,
type IssueComment,
hasLabel,
isBotUser,
listIssueComments,
} from "../github";
@@ -35,12 +36,12 @@ export async function checkStale(
now: Date = new Date(),
): Promise<Action[]> {
if (issue.milestone !== null) return [];
if (issue.labels.includes(LABELS.KEEP_OPEN)) 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 = issue.labels.includes(LABELS.STALE);
const hasStaleLabel = hasLabel(issue, LABELS.STALE);
const authorLogin = issue.user?.login ?? "there";
if (hasStaleLabel && daysSinceActivity < STALE_WARN_DAYS) {