Files
OpenFrontIO/scripts/issue-lifecycle
Evan f366f762cc Issue Lifecycle Actions (#4071)
## Description:

# Issue Lifecycle Actions

Adds two GitHub Actions workflows that enforce OpenFront's
issue-lifecycle invariants. No LLM calls — only the default
`GITHUB_TOKEN`. Layer B (Claude-powered triage) will build on this
foundation.

## Summary

- **Stale closer** — daily cron. Unmilestoned issues get warned at 5
days of inactivity, auto-closed at 10. Exempt: milestoned or
`keep-open`. Bot comments don't reset the timer.
- **Assignment invariant** — event + cron backstop. You cannot assign
anyone to an unmilestoned issue. Violators are unassigned automatically
with an explanatory comment.
- **Approval label sync** — event + cron backstop. The `not-approved`
(red) and `approved` (green) labels are derived from milestone state.
These labels are *only* ever touched by this Action.

## Rollout

Both workflows ship gated by `vars.ISSUE_LIFECYCLE_DRY_RUN` (defaults to
`'true'`). They log decisions but do not mutate anything until the
maintainer flips that variable in **Settings → Variables**.

Suggested rollout:
1. Merge with dry-run on.
2. Watch the cron logs for ~1 week. Verify the action list matches
expectations.
3. Flip `ISSUE_LIFECYCLE_DRY_RUN=false` to go live.

## File layout

```
.github/workflows/
  issue-lifecycle-cron.yml      # daily 06:00 UTC + workflow_dispatch
  issue-lifecycle-events.yml    # issues: [opened, assigned, milestoned, demilestoned]
scripts/issue-lifecycle/
  config.ts                     # labels, colors, thresholds, comment templates
  github.ts                     # Octokit wrapper, Action applier, label idempotent-creation
  rules/
    approval-label-sync.ts      # pure function — idempotent
    assignment-invariant.ts     # pure function
    stale-closer.ts             # async — reads comment history, filters bots
  cron.ts                       # daily sweep orchestrator
  events.ts                     # event-mode dispatcher
  index.ts                      # entrypoint, CLI arg parser
  README.md
```

Structure mirrors `scripts/pr-gate/` from Unit 2 — same Octokit/Action
patterns, same dry-run convention.

## Self-installing labels

On every run, the Action ensures the six labels exist (`not-approved`,
`approved`, `stale`, `keep-open`, `needs-info`, `auto-closed-stale`)
with the correct colors and descriptions. No manual setup required.

## Local testing

```bash
cd scripts/issue-lifecycle
npm install
export GITHUB_TOKEN=ghp_...

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

# Simulate an event:
EVENT_NAME=assigned npx tsx index.ts --mode event --issue 1234
```

CLI invocations are dry-run unless `--no-dry-run` is passed explicitly.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
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
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.