Markdown 371 lines
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/eeco_logo_dark.png">
<img src="assets/eeco_logo_light.png" alt="eeco" width="280">
</picture>
<h1>Versioning Policy</h1>
<p><i>The contract for how eeco is versioned, released, supported, and retired.</i></p>
<p>
<a href="README.md"><b>README</b></a> ·
<a href="VISION.md"><b>Vision</b></a> ·
<a href="docs/COCKPIT.md"><b>Cockpit</b></a> ·
<a href="docs/USAGE.md"><b>Usage</b></a> ·
<a href="docs/ARCHITECTURE.md"><b>Architecture</b></a> ·
<a href="docs/PUBLIC_API.md"><b>Public API</b></a> ·
<a href="EXTENDING.md"><b>Extending</b></a> ·
<a href="CONTRIBUTING.md"><b>Contributing</b></a> ·
<a href="docs/UPGRADING.md"><b>Upgrading</b></a> ·
<b>Versioning</b> ·
<a href="CHANGELOG.md"><b>Changelog</b></a> ·
<a href="SECURITY.md"><b>Security</b></a>
</p>
</div>
---
This document is the authoritative policy for how eeco is versioned,
released, supported, and retired. It is the contract every release of
eeco honours; a breach of any clause stated with **MUST** is a bug, not
a feature.
eeco is, at its first public release, **pre-stability**. It
re-launches publicly at `v0.1.0`; the most important thing this document does is mark that line
clearly and describe what changes — and what does not — when the
project crosses into stability at v1.0.0.
The keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and
**MAY** in this document are to be interpreted as described in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
## 1. Scope
This policy governs every artefact released under the
[`ajhahnde/eeco`](https://github.com/ajhahnde/eeco) GitHub repository
and the corresponding Homebrew and Scoop taps. It applies to every tag
of the form `vMAJOR.MINOR.PATCH`, including the current pre-stability
`v0.y.z` line, **with the explicit caveats** stated in §2.1 below.
## 2. Grammar
eeco follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html)
over the surface defined in §3. Until v1.0.0, the special pre-1.0 clause
of SemVer (§4) applies; see §2.1.
A release version is `vMAJOR.MINOR.PATCH` (with the leading `v`
preserved on every git tag, GitHub Release, and CHANGELOG section
header) optionally followed by a pre-release identifier as defined in
§7.
| Component | Trigger |
|---|---|
| **MAJOR** | A breaking change to any item enumerated in [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md); a default-value change that can cause an existing workflow to produce a materially different result; the removal of a previously deprecated feature. *(Reserved pre-1.0: the first MAJOR is v1.0.0, the stability-freeze release — see §2.1.)* |
| **MINOR** | Under pre-1.0 (§2.1): any change, including a breaking one, with the migration path called out in the CHANGELOG. Under post-1.0 (§2.2): an additive change only — a new command, flag, config key, builtin workflow, output field, or exit-code value — after which an existing workflow MUST observe identical behaviour unless it opts in to the new surface. |
| **PATCH** | A bug fix, performance improvement, documentation change, dependency bump, or internal refactor that does not touch the public surface. PATCH is **always** backwards-compatible, including pre-1.0. |
A change that fits more than one bucket MUST take the most disruptive
applicable bucket (e.g., a bug fix that, in fixing the bug, also
renames a flag is a MAJOR).
### 2.1 Pre-v1.0.0 (current line)
Under SemVer 2.0.0 §4 a major version of zero is for initial
development and "anything MAY change at any time". eeco adopts the
literal reading of that clause for its `v0.y.z` line, which is where
v0.1.0 re-launches the project:
- A **MINOR** bump (`v0.y.z` → `v0.(y+1).0`) MAY include a breaking
change to the surface enumerated in §3. Any such breaking change MUST
be called out in the CHANGELOG entry (under `### Changed`, with the
migration path) per the project's no-silent-breaking-changes rule.
- A **PATCH** bump (`v0.y.z` → `v0.y.(z+1)`) MUST NOT include a breaking
change to the surface enumerated in §3. PATCH is backwards-compatible
even pre-1.0.
- **No support guarantee** is made for any pre-v1.0.0 release. Only the
latest pre-v1.0.0 tag receives further attention; once v1.0.0 ships,
the entire pre-1.0 line enters the **Archived** tier of §8
permanently.
### 2.2 Post-v1.0.0 (future)
From v1.0.0 onward the standard SemVer interpretation applies without
the §2.1 carve-out: a breaking change MUST take a MAJOR bump. At that
point the deprecation procedure (§9) and the support tiers (§8) become
enforceable — in particular §9.3 ("removal MUST happen in a MAJOR")
governs the post-1.0 line, which is what reconciles it with the §2.1
rule that lets a pre-1.0 MINOR remove surface. Until then, §8 and §9
describe the model that takes effect at the stability freeze, not the
current `v0.x` line.
## 3. Public surface
The **frozen public surface** is the union of the items enumerated in
[`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). Nothing outside that
enumeration is part of the surface, and the policy MUST NOT be
interpreted to extend to anything else.
### 3.1 Stability classes
| Class | Discoverability | SemVer protection | Removal path |
|---|---|---|---|
| **GA** | Documented in [`docs/USAGE.md`](docs/USAGE.md) and [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). | Full. Breaking changes only in MAJOR with the deprecation window of §9. | Deprecate → window → remove in next MAJOR. |
| **Preview** | Documented as preview in [`docs/USAGE.md`](docs/USAGE.md); a runtime warning MUST be emitted on stderr the first time per process it is invoked. | None. A preview surface MAY change shape or be removed in any release, including a PATCH. | Drop without notice; CHANGELOG MUST record the removal. |
| **Internal** | Not documented in [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). The Go package surface under `internal/` is the canonical example. | None. | Drop without notice; not mentioned in the CHANGELOG. |
A surface MUST NOT be promoted from Preview to GA in a PATCH; the
promotion is a feature-add and goes in a MINOR.
### 3.2 Output stability
eeco is a CLI; its output channels carry different stability promises.
| Channel | Protection |
|---|---|
| **`--json` stdout** | Frozen top-level keys are enumerated in [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). Removing or renaming a frozen key is a MAJOR. Adding a frozen key is a MINOR. Nested object fields are **best-effort** and MAY gain keys in a MINOR; a frozen-nested-key contract is opt-in per command via [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). |
| **Human stdout** (default) | **Not** part of the public surface. Optimised for screen reading and SHALL change between MINORs whenever a presentation improvement is shipped. Scripts that parse the human form MUST switch to `--json`. |
| **Exit codes** | Documented in [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) §Workflow contract. Frozen. |
| **Stderr** | **Not** part of the public surface. Reserved for diagnostics, deprecation warnings, and operator hints. A consumer MUST NOT parse stderr. |
## 4. Release cadence
| Bump | Target cadence | Hard rule |
|---|---|---|
| **PATCH** | As-ready. A confirmed bug SHOULD reach a tagged PATCH within **7 days**. | Never blocked on a feature. |
| **MINOR** | Soft target of one per **~8 weeks**. No hard train — a MINOR ships when its surface is complete and the CHANGELOG entry is final. | Each MINOR MUST be additive over the previous MINOR within the same MAJOR (§2). |
| **MAJOR** | As-needed. A MAJOR MUST be announced under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md) and on the GitHub Releases page **at least 90 days** before its tag. The announcement enumerates every breaking change. | An RC train (§7) MUST precede the GA tag of any MAJOR. |
## 5. Branching and tagging
- The default branch is `main`. Every release tag is reachable from
`main` at the moment of tagging.
- A MAJOR line that is still under any support tier (§8) MUST have a
`release-X` branch (e.g., `release-1`) where its PATCH fixes are
prepared. The branch is force-push-protected.
- A release tag MUST match the regex `^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$`.
- A release tag MUST be annotated (`git tag -a`) and signed with the
release workflow's keyless cosign identity. The signed
`SHA256SUMS` and the SLSA build provenance produced by the release
workflow are part of the release artefact set.
- A release tag MUST NOT be deleted, force-moved, or re-pointed. The
yank procedure of §10 is the only sanctioned recall path.
- The `v0.1.0` initial release was a **one-time pre-policy reset**: the
prototype-era tags that preceded it were removed during the public
re-launch, before this policy governed the line. That reset is not a
§10 yank and not a breach of the no-delete rule above, which binds
only tags cut under this policy (`v0.1.0` onward).
## 6. Versioning of generated artefacts
Each release ships, at minimum:
- `eeco_<version>_<os>_<arch>.tar.gz` (and `.zip` for Windows) — the
pre-built binary archive, one per OS×arch in the published support
matrix.
- `SHA256SUMS` — the sha256 of every archive in the release, signed by
cosign keyless OIDC (the release workflow itself).
- A GitHub-built SLSA provenance attestation per archive.
- A Homebrew formula (`eeco.rb`) and a Scoop manifest (`eeco.json`)
published to the respective taps.
## 7. Pre-releases
A pre-release tag carries the suffix `-rc.N` (release candidate; `N`
is a strictly increasing non-negative integer starting at `0`).
`-alpha` and `-beta` pre-release identifiers are **not** used.
- An RC MUST be used for every MAJOR. It SHOULD be used for any MINOR
that materially changes default behaviour.
- An RC MUST be published to GitHub Releases marked as **pre-release**
and MUST NOT be pushed to Homebrew or Scoop.
- The RC train ends when the corresponding GA tag is cut from the same
commit as the last RC, with no behaviour change between the two.
- A user installing an RC accepts that the surface MAY differ from the
eventual GA by the contents of any further `-rc.N+1` issued for the
same target version. An RC is documented in the CHANGELOG under the
GA section that supersedes it; no separate `## [vX.Y.Z-rc.N]` section
is written.
## 8. Support windows
eeco's support model is **Node.js-LTS-inspired** with a single
operator and a 12-month maintenance trailing window after each MAJOR.
| Tier | Receives | Applies to | Ends |
|---|---|---|---|
| **Active** | Every bug fix, every applicable feature, every security fix. | The **current MAJOR**. | When the next MAJOR's GA ships. |
| **Maintenance** | **Security fixes** and **critical bug fixes** (data loss, crash on cold start, write-scope violation, signing/verification regression). No feature backports. No cosmetic changes. | The **previous MAJOR**. | **12 months after the next MAJOR's GA tag.** |
| **Archived** | Nothing. | Every MAJOR older than Maintenance. | Permanent. |
### 8.1 Current support table
| MAJOR line | Tier | First tag | Tier ends |
|---|---|---|---|
| **v0.x** (pre-stability) | None — §2.1 (no support guarantee; only the latest `v0.x` tag is current) | `v0.1.0` | When v1.0.0 GA ships — the pre-1.0 line then enters **Archived**. |
The current support table MUST be updated in the same PR that tags a
new MAJOR. The new line enters **Active**, the line that was previously
Active enters **Maintenance** with the EOL date computed as
`<new-MAJOR GA date> + 12 months`, and the line that was previously
Maintenance enters **Archived**.
### 8.2 What "Active" delivers
While a MAJOR is in **Active** support, eeco MUST publish:
- Every confirmed bug fix in a PATCH within the §4 cadence.
- Every applicable feature in a MINOR.
- Every applicable security fix in a PATCH (or, if the fix is
inherently breaking, in the next MAJOR with the §13 embargo timing).
### 8.3 What "Maintenance" delivers
While a MAJOR is in **Maintenance** support, eeco MUST publish:
- Every security fix that applies to the maintenance-line surface.
- Every critical bug fix as defined in the table above.
Maintenance PATCHes MUST NOT introduce a new flag, command, config
key, output field, or exit code. They MUST NOT change a default value.
### 8.4 Skew between Active and Maintenance
A workspace created by an **Active**-tier release MUST read cleanly
under any **Maintenance**-tier release within the same major or the
immediately preceding one. The converse — workspaces from a
Maintenance line reading on Active — is **always** supported and is
the standard upgrade path.
## 9. Deprecation policy
A change that will eventually break a frozen-surface item MUST go
through deprecation. The procedure follows the Kubernetes deprecation
model adapted for a single-operator project.
### 9.1 Announce
- A `### Deprecated` section is added to the next release's CHANGELOG
entry, naming each deprecated item, the replacement (if any), and the
earliest version in which the item MAY be removed.
- The deprecated item, when invoked, MUST emit a one-line warning on
stderr beginning with `eeco: DEPRECATED: ` and naming the
replacement.
- [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) MUST mark the item with a
`*(deprecated since vX.Y.0; removed in vM.0.0 or later)*` annotation.
### 9.2 Wait
The minimum window between the deprecation MINOR and the removal
release MUST be the longer of:
- **2 MINOR releases**, and
- **6 months** of wall-clock time.
The window does **not** restart when a deprecation is re-announced; the
clock runs from the first announcement.
### 9.3 Remove
- Removal MUST happen in a MAJOR. A PATCH or MINOR MUST NOT remove a
deprecated frozen-surface item.
- The release that removes the item MUST list it in the `### Removed`
CHANGELOG section and in the §1 of [`docs/UPGRADING.md`](docs/UPGRADING.md)
release entry.
### 9.4 Surfacing active deprecations
Every release MUST surface its still-active deprecations through:
- The `eeco doctor` output, listing each deprecated item the workspace
is currently using.
- The output of `eeco --help` for any command or flag that is
deprecated.
## 10. Yank and recall
A release MAY be yanked only for one of:
- A defect that destroys or corrupts user state.
- A signing or verification regression that breaks the release-artefact
trust chain.
- A security defect for which a fix cannot be issued within the §13
timeline.
The yank procedure:
1. The GitHub Release for the yanked tag is edited: title is prefixed
`[YANKED]`, body opens with one paragraph naming the yank reason and
the recommended replacement version. The release is marked
pre-release so it is no longer the latest.
2. The Homebrew tap and the Scoop tap are reverted to point at the
previous stable release within 24 hours.
3. The CHANGELOG entry for the yanked release is amended (in a
follow-up commit on `main`) to prepend a `**YANKED on YYYY-MM-DD**`
notice.
4. The fix is shipped as a follow-up PATCH within 48 hours of the yank.
5. The yanked tag is **never** deleted or force-moved. It remains for
audit and for users who pinned to it.
## 11. Security release policy
Vulnerability reporting and the safety guarantees in scope are
documented in [`SECURITY.md`](SECURITY.md). The version-policy
implications of a security release:
- A security PATCH MUST be issued to every MAJOR line currently in
**Active** or **Maintenance** tier (§8) that carries the defect.
- The default embargo window between report and tagged fix is
**90 days**, in line with Project Zero industry practice.
A shorter window MAY be negotiated on the advisory thread when the
fix is ready earlier.
- The CHANGELOG entry for a security PATCH MUST link the advisory
(typically a GitHub Security Advisory; a CVE identifier when one is
assigned).
- If the fix is inherently breaking and cannot be made
backwards-compatible, it MUST be shipped in the next MAJOR with an
exception note in the embargo agreement; the policy MUST NOT silently
break a maintenance-line workspace to deliver a security fix.
## 12. Roadmap signalling
A breaking change MUST NOT be a surprise.
- Every breaking change planned for the next MAJOR MUST be listed under
`## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md) **before** the
first RC of that MAJOR.
- The list is updated whenever a candidate breaking change is added or
removed; the diff itself is the public signal.
- The §4 ≥90-day announcement window starts from the date the
`[Unreleased]` block first contains the final list, not from when the
RC ships.
## 13. Governance
eeco is currently maintained by a single operator. A release MUST be
cut by:
1. A commit on `main` (or on a `release-X` branch for a Maintenance
PATCH).
2. Bumping the version anchors `version-sync` watches (see [`docs/USAGE.md`](docs/USAGE.md) §5).
3. Adding the release section to [`CHANGELOG.md`](CHANGELOG.md) and
the per-release entry to [`docs/UPGRADING.md`](docs/UPGRADING.md).
4. Tagging the commit `vX.Y.Z[-rc.N]` and pushing the tag, which
triggers the release workflow.
The release workflow MUST sign `SHA256SUMS` with cosign keyless OIDC
and produce the SLSA build provenance for every archive. A release
that fails the post-release `eeco update` self-verification on any
supported platform MUST be yanked (§10).
This policy MAY be amended; an amendment MUST itself follow the
versioning of this repository — a substantive change to the contract
is announced under `## [Unreleased]` and lands together with the next
release. A clarifying edit (typo, link rot, formatting) MAY land at
any time.
---
[← Prev: Upgrading](docs/UPGRADING.md) · [Next: Changelog →](CHANGELOG.md)