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.
- 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.xversions.
- Publication of a new crate version to crates.io, including
- 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.
- A crate whose major version is zero, for example
- 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.
- A crate whose major version is one or higher and which makes a supported API compatibility commitment.
It may also provide experimental APIs behind
- 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
unstablefeature, with the more limited compatibility commitments described below.
- Additional public API available through a non-default
- breaking change
- A change requiring a dependent crate to adapt, for example because code no longer compiles or documented behavior changes incompatibly.
Compatibility depends on a crate's API commitments, not on the functionality it provides.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 itscrate-status.mdchecklist. 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
gix1.0 rather than treating them as permanent support for exposed0.xcrates. - Keep experimental APIs behind a non-default
unstablefeature with datedgix_macros::unstablegates. 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.profilesand 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-checkand, when applicable,package.metadata.smart-release.unstable-featuresso 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.