Files
OpenFrontIO/scripts/issue-lifecycle
Evan 1cb84a79df 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>
2026-06-23 15:31:27 -07:00
..
2026-05-29 20:01:57 -07:00
2026-05-29 20:01:57 -07:00
2026-05-29 20:01:57 -07:00
2026-05-29 20:01:57 -07:00
2026-05-29 20:01:57 -07:00

Issue Lifecycle

Deterministic GitHub Actions that enforce OpenFront's issue lifecycle. No LLM calls — only the default GITHUB_TOKEN.

Rules

  1. Stale closer — daily cron. Unmilestoned issues get a warning at 7 days of inactivity, are closed at 14. Exempt: milestoned or keep-open-labelled issues. Assignees are not an exemption — if an unmilestoned issue somehow has assignees (event drift), Rule 2 unassigns them and Rule 1 still applies.
  2. Assignment invariant — event + cron. You cannot assign a person to an unmilestoned issue. Violators are unassigned automatically.
  3. Approval label sync — event + cron. not-approved and approved track milestone state (none = not-approved, any milestone = approved).

See the parent spec for the full contribution model rationale.

Triggers

Local testing

cd scripts/issue-lifecycle
npm install
export GITHUB_TOKEN=ghp_... # PAT with repo scope

# Full cron sweep against the real repo, dry-run (default for CLI):
npx tsx index.ts --mode cron

# Single-issue event-mode dry-run (simulates the assigned event):
EVENT_NAME=assigned npx tsx index.ts --mode event --issue 1234

# Force live mode locally (BE CAREFUL — this will mutate the repo):
npx tsx index.ts --mode cron --no-dry-run

CLI invocations are dry-run by default. Pass --no-dry-run to apply.

Toggling dry-run in production

  1. Go to repo Settings → Secrets and variables → Actions → Variables.
  2. Edit ISSUE_LIFECYCLE_DRY_RUN.
  3. Set to false to make the Actions act for real; any other value (or unset) keeps them in dry-run mode.

The default is true — both workflows log decisions but do not act until the maintainer flips the variable.

File layout

Notes

  • Labels are auto-created on every run (idempotent). The maintainer does not need to manually pre-create any of them.
  • Rules act only on issues — PRs are filtered out (pull_request field check).
  • API rate limit during the cron sweep causes the run to exit non-zero; the next day's cron retries.
  • No Claude / LLM calls. Layer B will layer triage on top of this foundation.