Skip to content

Measure stale unmarking from the stale window, not just the label date - #180

Merged
swoboda1337 merged 1 commit into
mainfrom
stale-consistent-activity-window
Aug 15, 2026
Merged

Measure stale unmarking from the stale window, not just the label date#180
swoboda1337 merged 1 commit into
mainfrom
stale-consistent-activity-window

Conversation

@swoboda1337

Copy link
Copy Markdown
Member

The unmark check asks whether there has been activity since the stale label was applied, while the marking check asks whether there has been activity in the last 90 days. Those are different questions, and once the label is older than the window both can be true at once — so an item is unmarked one night and marked again the next, posting a fresh stale comment every time.

esphome/esphome#10867 is a worked example:

2025-12-29 00:50  labeled stale       github-actions[bot]
2026-01-05 00:50  closed              github-actions[bot]
2026-01-05 01:05  comment "duh."      ireun
2026-01-13 00:48  locked              github-actions[bot]
2026-08-13 08:50  reopened, unlocked  ssieb
2026-08-14 00:52  unlabeled stale     esphome[bot]
2026-08-15 00:50  labeled stale       esphome[bot]

The reporter replied 16 minutes after the auto-close, and the issue was locked a week later. Locked items are skipped, so it sat untouched for seven months with the December label still attached. Reopening it put it back in the loop: that January comment was activity since the label, so the label came off on the 14th, and on the 15th the marking check found nothing inside 90 days and put it straight back on.

Measuring from whichever of the label date and the stale cutoff is later fixes it — activity that is itself outside the stale window can no longer clear the label. Items labelled in the normal course are unaffected, since a label applied days ago is always newer than the cutoff.

One consequence worth knowing: an item that is reopened while still carrying an old stale label now reaches the close branch on the next run rather than flip-flopping, and that branch posts no comment before closing. not-stale is the escape hatch, and removing the label when reopening also works. Counting a reopen as activity in its own right would handle that case properly, but it needs listEvents plumbed into hasRealActivitySince and is left for a follow-up.

The unmark check asked whether there was activity since the stale label
was applied, while the marking check asked whether there was activity in
the last 90 days. When the label was older than the window both could be
true at once, so an item was unmarked one night and marked again the
next, posting a fresh stale comment each time.

This happened to esphome/esphome#10867: labelled in December, commented
on six days later, then closed and locked. Reopening it in August let the
January comment clear the label, and the next run marked it stale again
because that comment was long outside the 90 day window.

Measure from whichever of the label date and the stale cutoff is later,
so activity that is itself stale can no longer clear the label.
@swoboda1337

Copy link
Copy Markdown
Member Author

@esphbot review

@esphbot

esphbot commented Aug 15, 2026

Copy link
Copy Markdown

@swoboda1337: @esphbot review

Reviewed. Clamp correct. No blockers.

Three non-blocking notes. Close log still prints labeledAt, not since — misleading for exactly these items. Reopen-as-activity cheaper than described: staleLabelDate already paginates all events, reopened sits in that payload. Clamp safety assumes days-before-close <= days-before-stale; inputs do not enforce that.

@esphbot

esphbot commented Aug 15, 2026

Copy link
Copy Markdown

PR Review — Measure stale unmarking from the stale window, not just the label date

Correct one-line fix for a real flip-flop bug, with a well-evidenced diagnosis. Merge-ready; three non-blocking notes.

The root-cause analysis is the strongest part of this PR: naming that the mark and unmark paths were asking two different questions is exactly right, and the #10867 timeline (locked for seven months with a December label still attached, then reopened) is a genuinely non-obvious path to that state. The Math.max-style clamp is the minimal fix — I verified it is a no-op for normally-labelled items, since with the default 90/7 configuration any label old enough for the clamp to bite (labeledAt < staleCutoff) is necessarily also past closeCutoff, so those items close on the same run rather than lingering in a half-checked state. Proactively documenting the reopen consequence in the description instead of leaving it to be discovered is the right call too.

  • The close branch's log still reports no real activity since stale on ${labeledAt} while the decision was made against since — for the targeted items that log line states something demonstrably false.
  • The deferred "count a reopen as activity" work is cheaper than described: staleLabelDate already paginates the full listEvents payload, which contains reopened events, so no new API call or signature change is needed.
  • The clamp's safety depends on days-before-close <= days-before-stale, which the workflow inputs don't enforce; an inverted configuration would ignore real post-label activity for the gap between the two windows.

🟢 Suggestions

1. Close log line now reports a date the check didn't use
.github/workflows/stale.yml:249-250

The close branch still logs no real activity since stale on ${labeledAt}, but after this change the decision was made against since, which for exactly the items this PR targets is staleCutoff, not labeledAt.

Why it matters: for #10867-style items the log will claim there was no activity since 2025-12-29, while the reporter demonstrably commented on 2026-01-05. Anyone auditing why an item got auto-closed reads a statement that is false, which is the worst kind of log line — it looks authoritative and sends the reader down the wrong path.

Fix: log the window actually used, e.g.

console.log(`Closing ${kind} #${item.number}: ` +
  `no real activity since ${since.toISOString()} ` +
  `(stale since ${labeledAt.toISOString()})`);

That requires hoisting since above the if/else if (it is currently declared inside the if scope's preceding statement, so it is already in scope for the else if branch — no restructuring needed).

console.log(`Closing ${kind} #${item.number}: ` +
  `no real activity since stale on ${labeledAt.toISOString()}`);
2. Clamp assumes `days-before-close <= days-before-stale`, which the inputs don't enforce
.github/workflows/stale.yml:234

The clamp is only benign because, with the defaults (90/7), labeledAt < staleCutoff implies labeledAt < closeCutoff — so every item whose since gets raised to staleCutoff is closed on the same run and can never linger in a state where post-label activity is ignored.

That implication breaks if a caller sets days-before-close greater than days-before-stale. Example: days-before-stale: 5, days-before-close: 30. An item labelled 10 days ago gets since = staleCutoff (5 days ago), so a genuine comment made 8 days ago — after the label — no longer clears it, and the item is not yet closable either. It sits stale for another 20 days and then closes despite real user activity.

This is a reusable workflow with public inputs, so the constraint is worth making explicit rather than implicit. Either document it on the days-before-close input description, or make it structural:

const since = labeledAt > staleCutoff ? labeledAt : staleCutoff;

→ only clamp when the item is already past the close threshold:

const since = labeledAt < closeCutoff && labeledAt < staleCutoff ? staleCutoff : labeledAt;

Not blocking — no known caller uses such a configuration, and the defaults are safe.

const since = labeledAt > staleCutoff ? labeledAt : staleCutoff;

Checklist

  • Fix addresses the described root cause
  • No unintended behavior change for normally-labelled items
  • Edge cases across configurable inputs — suggestion #2
  • Logging accurately reflects the decision made — suggestion #1
  • No scope creep — diff matches the description
  • No secrets, injection, or permission changes introduced

Automated review by Kōan (Claude) HEAD=4efc5c0 2 min 28s

@esphbot esphbot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

No blocking issues found — ready to merge.

@swoboda1337
swoboda1337 merged commit a1c1485 into main Aug 15, 2026
36 checks passed
@swoboda1337
swoboda1337 deleted the stale-consistent-activity-window branch August 15, 2026 13:00
@swoboda1337

Copy link
Copy Markdown
Member Author

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants