Skip to main content

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_revisions key under the top-level info dictionary.
  • A top-level v3 key.

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, where N MUST be 3 or 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 not None, 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 not None, it MUST match the sum of all the resulting records found under the vN key in the current repodata file or shard.
    • newest: int | None: If present and not None, a timestamp (in milliseconds) that MUST match the indexed_timestamp field of the newest record published in this revision in the current repodata file or shard.
    • oldest: int | None: If present and not None, a timestamp (in milliseconds) that MUST match the indexed_timestamp field 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.bz2 and conda).
  • 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_timestamp field introduced by CEP 47 SHOULD be set.
      • The extra_depends field introduced by CEP 44 MAY be present.
      • The MatchSpec strings mentioned in the fields depends, constrains and the lists of strings within extra_depends groups:
        • MUST set the name field to an exact string (no globbing allowed).
        • MAY set the fields: version, build, build_number, CEP 43 when, CEP 44 extras, CEP 45 flags.
        • MUST NOT set any other fields.
        • If only name is 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 name is set, the MatchSpec MUST use the name + square-brackets form (e.g. name[version="1.2.*",build_number=0]).

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:

  • rattler supports it since v0.9.0 (released on 2023-09-22), which means that pixi supports it since v0.4.0 (released on 2023-09-22 too).
  • conda only supports it as of v24.5.0 (released on 2024-05-08)
  • mamba started 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.json versions and their shards.
  • Indexing tools would need to maintain the different versions of repodata.json in 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.

All CEPs are explicitly CC0 1.0 Universal.