CEP 48 - A backwards-compatible update strategy for repodata v3
| Title | A backwards-compatible update strategy for repodata v3 |
| Status | Accepted |
| Author(s) | Jaime Rodríguez-Guerra <jaime.rogue@gmail.com>, Bas Zalmstra <bas@prefix.dev>, Wolf Vollprecht <w.vollprecht@gmail.com> |
| Created | Jan 12, 2026 |
| Updated | Aug 21, 2026 |
| Discussion | https://github.com/conda/ceps/pull/146 |
| Implementation | N/A |
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119 when, and only when, they appear in all capitals, as shown here.
Abstract
This document proposes a set of updates to repodata.json files and its derivatives (sharded, subsets) to include the improvements introduced in CEPs 43, 44, 45, and 47. To do so in a backwards compatible manner, it also proposes a revision system as a way to extend repodata with new additions, without incrementing repodata_version.
Motivation
repodata.json files are central to the conda ecosystem. They are the main source of packaging metadata and inform solvers about the catalog of available packages and their dependency constraints. As such, innovation work often refrains from modifying it, and the format itself has seen very few changes over its lifetime. However, the adoption of CEPs 43, 44, 45 and 47 will inevitably result in repodata.json modifications (conditional dependencies, optional dependency groups, non-conda dependencies, etc).
The main problem is the introduction of backwards incompatible changes. The obvious solution is to bump the repodata_version field (like it was done with CEP 15). However, this is not desirable for existing channels, since it immediately prevents non-compatible clients from interacting with the channel. Since most clients would update via a new version available in the channel, it creates a chicken-and-egg problem that would significantly delay the introduction of new features and hinder adoption.
There must be a strategy to introduce backwards incompatible changes without breaking existing channels. This CEP centralizes the discussion for the update strategy by introducing the concept of revisions and consolidates that feedback into a concrete proposal for v3.
The v3 update includes breaking changes in CEPs 43, 44, 45, and 47 that wouldn't otherwise reach existing channels without disrupting the user experience for outdated clients.
Specification
This CEP introduces two new keys:
- A
repodata_revisionskey under the top-levelinfodictionary. - A top-level
v3key.
The info.repodata_revisions key
This key MUST map to a dictionary where:
- Each key MUST correspond to a newly introduced top-level key with syntax
vN, whereNMUST be3or a larger integer. - Each value MUST be a dictionary with the following optional key-value pairs. Additional keys SHOULD be ignored.
message: str | None: If present and notNone, a free-form string to be set by channel operators to provide contextual information (e.g., announcements, deprecation notices, or a change in client requirements). Its length MUST NOT exceed 8192 bytes.n_packages: int | None: If present and notNone, it MUST match the sum of all the resulting records found under thevNkey in the current repodata file or shard.newest: int | None: If present and notNone, a timestamp (in milliseconds) that MUST match theindexed_timestampfield of the newest record published in this revision in the current repodata file or shard.oldest: int | None: If present and notNone, a timestamp (in milliseconds) that MUST match theindexed_timestampfield of the oldest record published in this revision in the current repodata file or shard.
The info.repodata_version value MUST be 1 or, if CEP 15 applies, 2.
The v3 top-level key
This key MUST map to a dictionary of type dict[str, dict]:
- Each key MUST be a non-empty string. The key SHOULD represent the file extension (without the leading period) of the included artifacts (usually
tar.bz2andconda). - Each value MUST be a dictionary of type
dict[str, dict]where:- Each subkey MUST be a non-empty string representing the artifact filename without its extension.
- Each subvalue MUST be a valid CEP 36 "package record metadata" dictionary, including these changes:
- The
indexed_timestampfield introduced by CEP 47 SHOULD be set. - The
extra_dependsfield introduced by CEP 44 MAY be present. - The MatchSpec strings mentioned in the fields
depends,constrainsand the lists of strings withinextra_dependsgroups:- MUST set the
namefield to an exact string (no globbing allowed). - MAY set the fields:
version,build,build_number, CEP 43when, CEP 44extras, CEP 45flags. - MUST NOT set any other fields.
- If only
nameis set, the MatchSpec MUST be the bare name string (e.g.pip). Empty brackets (e.g.pip[]) MUST NOT be used. - If any field other than
nameis set, the MatchSpec MUST use thename+ square-brackets form (e.g.name[version="1.2.*",build_number=0]).
- MUST set the
- The
Rationale
Version metadata in info
Adding a new field in the info dictionary is backwards compatible, and can be used by clients to parse the necessary keys directly without having to traverse the whole document. The oldest, newest, and n_packages fields are useful for client messaging, like "the client is not recent enough to see all records in this channel, please update to ensure you can obtain access to {n_packages} additional packages published between {oldest} and {newest}". They are not added as a top-level field to stop polluting the global namespace.
Using top-level fields for new metadata schemas
Adding new fields is backwards compatible and does not break older clients, which will simply ignore those and continue operating as usual.
Freezing repodata_version to 1
Bumps in this number should only result in backwards incompatible changes that would anyway prevent a channel from operating completely. While repodata_version: 2 exists (as per CEP 15), its implementations are not sufficiently old to guarantee that the majority of existing conda clients would support it:
rattlersupports it since v0.9.0 (released on 2023-09-22), which means thatpixisupports it since v0.4.0 (released on 2023-09-22 too).condaonly supports it as of v24.5.0 (released on 2024-05-08)mambastarted supporting it in v2.0 (released on 2024-09-25).
Hence, we recommend sticking to repodata_version: 1 and only using repodata_version: 2 when a new channel needs a global base_url for all the entries in the packages and packages.conda fields.
Example
{
"repodata_version": 1,
"info": {
"subdir": "noarch",
"repodata_revisions": {
"v3": {
"message": "This is just an example v3 revision",
"n_packages": 1,
"oldest": 1773851561010,
"newest": 1773851561010
}
}
},
"packages": {
"example-1.0.0-0.tar.bz2": {
"build": "0",
"build_number": 0,
"depends": [],
"md5": "82ecc40f09b9c44483e6b70cad2545d7",
"name": "example",
"noarch": "generic",
"sha256": "eb65e866067865793b981c2ba74485f75bef441842b5998badc4ec66717685c7",
"size": 1234,
"subdir": "noarch",
"timestamp": 1689209309623,
"version": "1.0.0"
}
},
"packages.conda": {
"package-1.0.0-0.conda": {
"build": "0",
"build_number": 0,
"depends": [],
"md5": "4483e6b70c82ecc40f09b9c4ad2545d7",
"name": "package",
"noarch": "generic",
"sha256": "4485f75bef441842b59eb65e866067865793b981c2ba798badc4ec66717685c7",
"size": 1234,
"subdir": "noarch",
"timestamp": 1689209359623,
"version": "1.0.0"
}
},
"v3": {
"tar.bz2": {},
"conda": {
"example-3.0.0-0": { // key does not have the extension anymore
"build": "0",
"build_number": 0,
"depends": [
"package[version=2,build_number=0,when=__unix]" // bracket syntax, w/ conditional
],
"extra_depends": { // NEW
"test": [
"test-dependency"
]
},
"md5": "6b70cad2545d782ecc40f09b9c44483e",
"name": "example",
"noarch": "generic",
"sha256": "74485f75bef441842b5998badc4ec66717685c7eb65e866067865793b981c2ba",
"size": 2345,
"subdir": "noarch",
"timestamp": 1773851540030,
"indexed_timestamp": 1773851561010, // NEW
"version": "3.0.0"
}
}
}
}
Rejected ideas
Encoding version information in the filename
One alternative would be to create new repodata.json filenames (e.g. repodata.v4.json) for each new incompatible bump. However, this was rejected by the authors as it introduces complexity in other areas:
- It would require more HTTP calls to retrieve the latest version served by the channel.
- It would introduce duplication across
repodata.jsonversions and their shards. - Indexing tools would need to maintain the different versions of
repodata.jsonin sync.
Nested name-version-build_string dictionaries
A review comment proposed subdividing the dictionary of filenames to records in nested dictionaries. In other words, instead of listing the artifact package-version-build.conda as:
{
...,
"v3": {
"conda": {
"example-2.0.0-0": {
... // record dictionary
}
}
}
}
... expose it as:
{
...,
"v3": {
"conda": { // extension
"example": { // name
"2.0.0": // version
"0": { // build string
... // record dictionary
}
}
}
}
}
}
This would simplify parsing of records, but would also complicate the enumeration of existing records.
Expose MatchSpec entries in records as dictionaries
MatchSpec parsing in depends and other record fields has a non-negligible cost that may add up over thousands of records. Instead of keeping these fields as list[str] where each string is a MatchSpec expression, it was suggested to expose each entry as a dictionary of MatchSpec fields.
For example, for this input index.json:
{
"build": "0",
"build_number": 0,
"depends": [
"package[version=3,build_number=0,when=__unix]"
],
"name": "example",
"noarch": "generic",
"subdir": "noarch",
"version": "3.0.0"
}
... depends would adopt this form:
{
...,
"depends": [
{
"name": "package",
"version": "=3",
"build_number": 0,
"when": "__unix"
}
],
...
}
This would involve bigger changes in the implementations, delaying the adoption of v3.
Changelog
- 2026-08-21: CEP was accepted.
Copyright
All CEPs are explicitly CC0 1.0 Universal.