How it works

Python Go Rust Java C / C++ JS / TS

Real rules, not vague TODOs

A TODO: remove later outlives everyone who wrote it. Expiry ties deletion to checkable signals — ticket closed, library upgraded, flag shipped — in any language.

Single-line annotations

One comment next to the hack. Mix conditions with | — issue state, dependency semver, flags, env vars.

  • issue:ENG-442:closed
  • dep:react@>=19.0.0
  • flag:NEW_UI:enabled
  • env:LEGACY_MODE!
src/auth.ts
// @expiry issue:ENG-442:closed | remove legacy prefix check
export function legacyAuth(t: string) {
  return t.startsWith("legacy_");
}

Every condition type

Evaluated on every CI run. Combine with | in one annotation.

issue:KEY:state

GitHub or Jira issue open/closed.

dep:pkg@range

Semver check against your lockfile.

flag:NAME:state

Feature flag enabled/disabled via env.

env:VAR / env:VAR!

Variable present, equals a value, or absent.

file:path

Wait for a migration artifact to land.

date:YYYY-MM-DD

Hard deadline when you need one.

Runs in your existing CI

expiry init drops a GitHub Actions workflow. On every PR, Expiry scans annotations, evaluates conditions, posts a health score comment, and fails if anything is overdue.

CI setup guide →
.github/workflows/expiry.yml
- uses: Llunarstack/expiry/action@v1
  with:
    license-key: ${{ secrets.EXPIRY_LICENSE_KEY }}
    fail-on-overdue: true
    comment-on-pr: true
    auto-cleanup-pr: true  # Pro