Skip to content

Milestone policy — delivery, epics and evergreen issues

Status: Maintainer policy, decided under #480 (parent epic #377). Enforced by: scripts/milestone_policy.py, offline, on an issue snapshot.

A GitHub milestone in this repository is an outcome: something a release can finish, with a progress bar that can reach 100%. Not every open issue is work of that shape. A call for compatibility reports, a translation review queue or an open research question is never "done" in the way an outcome is, and putting it in a milestone holds that outcome's progress below 100% for ever. So the rule is not "every open issue has a milestone". It is:

Every open issue is in exactly one class, chosen by its labels. The class says whether it carries a milestone. Nothing falls through.

This is option B of #480 (typed exceptions), with the exception carried in a label rather than inferred from the title.

The three classes

Class Marked by Milestone Example
Delivery no class label (the default) exactly one, and it must be open a bug, a feature, a bounded docs task, a validation slot
Epic the epic label derived from its sub-issues (below) #81 Android, #377 traceability
Evergreen exactly one evergreen:<class> label none a "test it on your distro" call

Delivery is the default on purpose. An issue nobody classified is treated as work that owes a milestone, so forgetting a label produces a violation, not a silent exemption.

Evergreen classes

The set is closed. A new class is a change to this page and to EVERGREEN_CLASSES in scripts/milestone_policy.py; a test fails if the two disagree.

Label What belongs there
evergreen:research An open research question with no delivery outcome of its own — the public research agenda (#251–#258). A bounded study inside a programme (a protocol to freeze, a matrix to run) is delivery work in that programme's milestone.
evergreen:field-report An open-ended call for compatibility or measurement reports from many people: the per-distro "test YazSes and report" issues, the "Measure it" collectors. A fixed validation slot with an acceptance role (the eye-control EYE-QA-* slots) is delivery work.
evergreen:translation A translation or native-speaker review queue that stays open for volunteers. A bounded localization task (change these 24 pages) is delivery work.
evergreen:community A contributor hub, a stewardship invitation, an outreach call.

Topical labels such as research, measurement-wanted or documentation say what an issue is about; they never exempt it. Only an evergreen:* label does, so adding a topic label to a bug cannot quietly take it out of a milestone.

Epics and their children

An epic coordinates sub-issues, and the sub-issues carry the delivery milestones. The epic's own milestone follows from them:

  • if every open delivery sub-issue sits in the same milestone, the epic carries that one;
  • if they span several milestones, the epic carries none — it is a programme, not an outcome (Android #81 has open sub-issues in all four milestones M0–M3);
  • children are linked as GitHub sub-issues, not as a list in the epic's body: prose cannot be checked, and a policy that cannot be checked drifts.

An epic is never also evergreen: one class per issue.

Answering the four questions in #480

  1. Is this issue expected to have a milestone? Delivery: yes, exactly one. Evergreen: no. Epic: whatever its open sub-issues share, or none if they span outcomes.
  2. If yes, which outcome owns it? The milestone it carries. A delivery issue with no fitting outcome is a signal that an outcome is missing, not that the issue is exempt — propose the milestone.
  3. If no, what exempts it? Its single evergreen:<class> label, or the epic label together with sub-issues that span milestones.
  4. Can an issue fall through both? No. With no class label it is delivery and needs a milestone; with an unknown or a second class label it is a violation.

What is checked

scripts/milestone_policy.py reads an open-issue snapshot (gh issue list --json output or the REST issues payload) and an optional milestone listing, and reports:

Rule Fires when
DELIVERY_WITHOUT_MILESTONE a delivery issue has no milestone
EVERGREEN_WITH_MILESTONE an evergreen issue carries one
UNKNOWN_EVERGREEN_CLASS an evergreen:* label names no defined class
CONFLICTING_CLASSES an issue carries more than one class label
MILESTONE_UNKNOWN / MILESTONE_CLOSED the milestone is missing from the listing, or closed (only with a listing)
EPIC_SPANS_MILESTONES an epic carries a milestone that some open delivery child does not
EPIC_MILESTONE_MISSING an epic has none although all its open delivery children share one

Epic rules need parent links in the snapshot (a parent field per issue). Without them the report says so and lists each epic as unverified; it never reports them as passing.

gh issue list --state open --limit 1000 --json number,title,labels,milestone > issues.json
gh api repos/MSKazemi/yazses/milestones -X GET -f state=all --paginate > milestones.json
uv run python scripts/milestone_policy.py issues.json --milestones milestones.json
# or, together with the manifest check:
uv run python scripts/check-traceability.py --issues issues.json --milestones milestones.json

Exit codes: 0 no violation, 1 violations, 2 an input could not be read. The unit tests (tests/test_milestone_policy.py) run on fixtures and stay offline; fetching live state is the job of the scheduled live audit (#487).

Why not an "Evergreen" milestone

The other way to make every issue carry a milestone is a catch-all bucket milestone. It was rejected: a milestone that can never close is the thing this policy exists to avoid, its progress bar would mean nothing, and it would make question 1 always "yes" without making question 2 any easier to answer.