Skip to content

Latest commit

 

History

History
234 lines (176 loc) · 13.7 KB

STABILITY.md

File metadata and controls

234 lines (176 loc) · 13.7 KB

Stability

This project follows semver. This guide explains which APIs carry compatibility commitments, when crates must make those commitments, and how changes to supported APIs and experimental APIs affect version numbers.

The guide itself may evolve as requirements change.

Terminology

  • dependent crate
    • A crate which depends directly on a crate in this workspace.
  • downstream crate
    • A crate which directly or indirectly depends on a crate in this workspace.
  • release
    • Publication of a new crate version to crates.io, including 0.x versions.
  • major-zero crate
    • A crate whose major version is zero, for example 0.1.4. It may introduce breaking changes in minor releases. This says nothing about its quality, completeness, or suitability for production use.
  • major-positive crate
    • A crate whose major version is one or higher and which makes a supported API compatibility commitment. It may also provide experimental APIs behind unstable.
  • supported API
    • The public API for which a major-positive crate makes normal semver compatibility commitments.
  • experimental API
    • Additional public API available through a non-default unstable feature, with the more limited compatibility commitments described below.
  • breaking change
    • A change requiring a dependent crate to adapt, for example because code no longer compiles or documented behavior changes incompatibly.

Versioning and public exposure

Compatibility depends on a crate's API commitments, not on the functionality it provides.

Implementation-only major-zero crates

A crate may remain a major-zero crate indefinitely if it remains an implementation detail and is never exposed through another crate's public API. Neither production use nor publication on crates.io requires it to reach 1.0. Breaking changes in a major-zero crate require a minor version bump, for example from 0.1.4 to 0.2.0.

Calling a dependency internally does not expose it. A dependent crate can hide a major-zero crate behind its own API and types, and update or replace that dependency without changing its supported API.

Publicly exposed crates

A crate exposed through another crate's public API must become a major-positive crate and support the exposed API. Keep its remaining experimental APIs behind its non-default unstable feature. Private implementation code needs neither stabilization nor an unstable gate.

Exposure includes re-exports and types reachable through fields, variants, methods, associated types, bounds, and trait implementations, not only direct function signatures. Exposing a major-zero crate behind unstable still exposes it; gating does not let it remain a major-zero crate indefinitely. A supported API must not depend on an experimental API, even when feature unification makes it public.

The same rule applies to workspace and external dependencies. Existing departures from it are covered by temporary migration exceptions, not a permanent alternative to stabilization.

Release conventions

A release must use the appropriate version increment and pass the required checks.

Once gix is a major-positive crate, the naming convention appends the release year and month as +YY.MM, for example 1.2.3+26.11. This is semver build metadata: it does not affect version precedence or compatibility requirements.

Supported and experimental public APIs

A major-positive crate need not stabilize every capability. It commits to a chosen supported API while allowing additional experimental APIs behind a non-default unstable feature. These rules apply to all major-positive crates, including gix once it reaches 1.0.

Version increments

For a major-positive crate, the minimum version increment depends on the changes included in the release:

Change Minimum increment Example from 1.2.3
Backward-compatible fixes or internal changes Patch 1.2.4
New public functionality without breaking the supported API Minor 1.3.0
Breaking changes confined to experimental APIs Minor 1.3.0
Breaking changes to the supported API Major 2.0.0

Patch releases must preserve both supported APIs and experimental APIs. A release containing multiple kinds of changes takes the strongest required increment. Explicit major bumps and breaking commit intent retain any stronger release requirement.

A minor release need not add features. It may contain only breaking changes behind unstable while leaving the supported API unchanged. Downstream crates using only the supported API can accept that update normally; those using experimental APIs must review it before upgrading.

Consuming experimental APIs

Downstream applications using a workspace crate solely as an implementation detail may opt into its experimental APIs, but must restrict updates to the same minor series. Once gix is a major-positive crate, for example:

gix = { version = "~1.2.3", features = ["unstable"] }

This admits 1.2.x updates from 1.2.3 onward but excludes 1.3.0, protecting the dependent crate from accidental breaking changes to experimental APIs. Plain "1.2.3" permits minor updates and lacks that protection. An exact requirement such as "=1.2.3" also protects against those changes, but excludes patch updates too. This minor-series requirement also applies within this workspace. Each dependent crate must constrain any dependency whose experimental APIs it uses; pinning the dependent crate itself does not constrain that dependency's minor series.

Downstream libraries outside this workspace that expose a workspace crate or its types in their supported API should use only that dependency's supported API, including internally, rather than enable or forward unstable. This avoids imposing an experimental minor-series pin on the libraries' own downstream crates using those types. These downstream libraries can accept normal semver-compatible updates:

gix = { version = "1.2.3" }

Downstream applications using those libraries can opt into the dependency's experimental APIs independently. Those experimental APIs must not become part of a downstream library's supported API.

Gating experimental APIs

gix_macros::unstable keeps annotated items available within the crate while exposing them publicly only with its unstable feature enabled. Gate experimental members as well as whole types or modules where needed; supporting a type does not imply supporting every member it could expose.

Every gate requires #[gix_macros::unstable(since = "YYYY-MM-DD")]. Use the item's introduction date from Git history, following renames when necessary. The date must be a valid calendar date and is included in experimental documentation to record API provenance. Usually the date is sufficient; add a nonempty reason = "..." only when it explains something beyond the signature or the fact that the item is experimental.

Temporary migration exceptions

The workspace is still transitioning to the public-exposure rule. Until gix reaches 1.0, existing manifest-declared exceptions provisionally support selected types from major-zero crates. These are migration allowances, not permission for publicly exposed dependencies to remain major-zero crates forever. Before releasing gix 1.0, make the exposed crates major-positive crates or remove their exposure. Gate their remaining experimental APIs behind non-default unstable.

Each dependent crate's Cargo manifest is the source of truth for its exceptions:

[package.metadata.api-check]
allowed-unstable-types = ["dependency_crate::BasicType"]
# Reserve whole-crate exceptions for APIs intentionally supported in their entirety.
allowed-unstable-crates = []

Use fully qualified Rust type paths without generic arguments, and Rust crate names with underscores. A type exception applies to that type, not its entire crate. An API does not require the enclosing crate's unstable feature solely because it exposes a declared exception. Other types exposed by supported fields, variants, methods, associated types, bounds, and trait implementations still need their own assessment.

An exception permits exposure; it does not establish compatibility across breaking dependency releases. Compatible 0.x dependency updates remain allowed. Breaking changes to the supported API or exposed type identity still require a major release when the dependent crate is a major-positive crate. Release API checks must retain those checks even for exempt dependencies.

API checks

Release planning compares the supported API and the full API separately. Supported comparisons cover the feature profiles declared in package.metadata.api-check.profiles, excluding features listed in package.metadata.smart-release.unstable-features. Full comparisons enable all those experimental features together on each profile and include experimental APIs. The declared features must work together; a failed check aborts planning rather than establishing compatibility.

Full API compatibility permits patch releases; breaking changes confined to experimental APIs require at least a minor increment. Breaking changes to a major-positive crate's supported API require a major increment. Stronger version requirements are preserved.

Standalone compare and boundary checks cover only the supported API.

etc/scripts/check-api.sh boundary path/to/Cargo.toml checks these profiles against the manifest's exceptions. Direct checks inspect the dependent crate's API and the stability of its referenced dependency types. Set package.metadata.api-check.recursive = true to also follow the supported members of reachable workspace dependency types; this is planned for gix. Experimental members are excluded using the documentation markers emitted by gix_macros::unstable, regardless of whether feature unification made them public. Dependency rustdoc is generated with the features resolved for each profile of the dependent crate.

These boundary checks supplement release compatibility checks. Because they inspect supported profiles, also review experimental APIs for exposed major-zero crates: gating alone does not satisfy the public-exposure rule.

Downstream compatibility testing

CI should build and test major downstream crates against candidate workspace changes before a release. This surfaces unintended source and behavioral breakage early and lets us validate downstream adaptations for intentional breaking changes.

These checks complement workspace tests and API-compatibility checks; they do not relax version-bump requirements. Where practical, deprecate APIs in compatible releases before removing or changing them so downstream crates can migrate.

Minimum supported Rust version (MSRV)

Each crate declares its MSRV through package.rust-version in its Cargo manifest. The gix crate and its dependency graph are tested at its MSRV by the ci.yml workflow.

MSRV increases are assessed per crate and are made only when adopting a newer Rust version improves the quality of that specific crate's code. Such increases are not considered breaking changes under this project's versioning policy. API compatibility, including a patch release's experimental API guarantee, does not imply an unchanged compiler requirement.

Please let us know if you need a different compiler support policy so we can consider accommodating it.

Establishing a supported API

Implementation-only major-zero crates need not become major-positive crates. Public exposure requires that transition, but not completion of every feature. Stabilize the API that downstream crates need; the remaining experimental API can stay behind unstable. The gix-fs 1.0 API illustrates this approach.

Before exposing a crate as part of a public API:

  • Define the supported API and assess it against the needs of downstream crates. Resolve blockers in any "<crate-name> towards 1.0" tracking issue and review its crate-status.md checklist. Explicitly defer work outside that scope instead of requiring every planned feature to be complete.
  • Ensure the crate and its publicly exposed dependencies are major-positive crates. Keep implementation-only dependencies private; they may remain major-zero crates. Resolve existing migration exceptions before gix 1.0 rather than treating them as permanent support for exposed 0.x crates.
  • Keep experimental APIs behind a non-default unstable feature with dated gix_macros::unstable gates. Review types and their members, bounds, and implementations. Supported APIs must not require experimental APIs even if dependency feature unification makes them public.
  • Declare package.metadata.api-check.profiles and pass their boundary checks. Enable recursive checking where transitive commitments are intended, and review the dependency exposure of experimental APIs separately.
  • Configure package.metadata.smart-release.api-check and, when applicable, package.metadata.smart-release.unstable-features so release planning checks the supported API and the full API. Constrain dependencies providing experimental APIs to a minor series, and run the relevant tests, builds, documentation checks, and CI checks of downstream crates.

A first stable release has no historical stable baseline; passing boundary checks does not establish compatibility with earlier releases of major-zero crates. Establishing a supported API does not require stabilizing implementation-only dependencies or all dependent crates.