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:closeddep:react@>=19.0.0flag:NEW_UI:enabledenv:LEGACY_MODE!
// @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.
- 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