mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 18:22:43 +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>
Issue Lifecycle
Deterministic GitHub Actions that enforce OpenFront's issue lifecycle. No LLM calls — only the default GITHUB_TOKEN.
Rules
- 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. - Assignment invariant — event + cron. You cannot assign a person to an unmilestoned issue. Violators are unassigned automatically.
- Approval label sync — event + cron.
not-approvedandapprovedtrack milestone state (none =not-approved, any milestone =approved).
See the parent spec for the full contribution model rationale.
Triggers
- issue-lifecycle-cron.yml — daily at 06:00 UTC, plus
workflow_dispatch. - issue-lifecycle-events.yml —
issues: [opened, assigned, milestoned, demilestoned].
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
- Go to repo Settings → Secrets and variables → Actions → Variables.
- Edit
ISSUE_LIFECYCLE_DRY_RUN. - Set to
falseto 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
- config.ts — constants, labels, comment templates
- github.ts — Octokit wrapper, action applier, label idempotent-creation
- rules/approval-label-sync.ts — Rule 3 (pure function)
- rules/assignment-invariant.ts — Rule 2 (pure function)
- rules/stale-closer.ts — Rule 1 (async; reads comment history)
- cron.ts — daily sweep orchestrator
- events.ts — event-mode dispatcher
- index.ts — entrypoint, arg parser
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_requestfield 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.