From 1cb84a79df8504e6c099cf1ab2043ef07e215507 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 23 Jun 2026 15:31:27 -0700 Subject: [PATCH] Fix stale bot re-commenting on case-mismatched labels (#4394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- scripts/issue-lifecycle/github.ts | 8 ++++++++ scripts/issue-lifecycle/rules/approval-label-sync.ts | 6 +++--- scripts/issue-lifecycle/rules/stale-closer.ts | 5 +++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/issue-lifecycle/github.ts b/scripts/issue-lifecycle/github.ts index 30a37da7d..7a2321f23 100644 --- a/scripts/issue-lifecycle/github.ts +++ b/scripts/issue-lifecycle/github.ts @@ -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 { diff --git a/scripts/issue-lifecycle/rules/approval-label-sync.ts b/scripts/issue-lifecycle/rules/approval-label-sync.ts index a7da478c7..199dc752d 100644 --- a/scripts/issue-lifecycle/rules/approval-label-sync.ts +++ b/scripts/issue-lifecycle/rules/approval-label-sync.ts @@ -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[] = []; diff --git a/scripts/issue-lifecycle/rules/stale-closer.ts b/scripts/issue-lifecycle/rules/stale-closer.ts index fbfb22609..895f4cb8e 100644 --- a/scripts/issue-lifecycle/rules/stale-closer.ts +++ b/scripts/issue-lifecycle/rules/stale-closer.ts @@ -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 { 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) {