Skip to content
ocx
install

Versioning

This page explains how OCX tags, variants and platforms compose into a single package identifier.

Every binary package has three dimensions: what it is, which version, and which platform build. Most ecosystems handle these separately and leave the coordination to the consumer. Apt uses arch suffixes and ports mirrors. Pip uses platform-encoded wheel filenames. Bazel rules use hand-curated URL matrices.

OCX collapses all three dimensions into a single identifier. kitware/cmake:3.28 resolves the right binary for the current machine, verifies it cryptographically, and installs it. It needs no flags, name suffixes, or mirror configuration.

The user-facing surface lives in the Versioning section of the user guide. It covers how to pick a tag, pin a digest, and override the platform.

An OCI tag is a human-readable label pointing to a specific manifest. The OCI Distribution Specification offers no notion of tag stability, because tags are mutable pointers. Structure and stability emerge from publishing conventions.

Rolling tags are a widespread pattern in container ecosystems. Docker official images use them extensively (ubuntu:24.04, node:22, nginx:latest). Semantic Versioning formalizes the same hierarchy for software releases. OCX adopts both conventions for binary packages.

OCX follows a semver-inspired hierarchy where specificity signals intent:

{major}[.{minor}[.{patch}[-{prerelease}][_{build}]]]

The _build suffix is typically a UTC timestamp like _20260216120000. It signals that the publisher considers the tag final and does not intend to re-push it. It is a convention, not a guarantee enforced by the registry.

TagPublisher’s intentAfter index refresh
kitware/cmake:3.28.1_20260216120000Do not re-pushSame build (by publisher convention)
kitware/cmake:3.28.1Rolling patchHighest build of 3.28.1
kitware/cmake:3.28Rolling minorNewest 3.28.x build
kitware/cmake:3Rolling majorNewest stable 3.x build
kitware/cmake:latestFloatingNewest release

Semantic Versioning uses + to delimit build metadata, for example 1.2.3+20260216. The OCI Distribution Specification restricts tags to [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}, so the + character is not allowed.

This is a known ecosystem-wide issue (distribution-spec#154, open since 2020 and unresolved). The + also encodes as a space in URL query strings, which makes it unsafe even if registries accepted it.

OCX uses _ as the build separator, so every version string is a valid OCI tag by construction. The grammar above is fully unambiguous: . separates components, - introduces a pre-release, and _ introduces build metadata.

Input is tolerant. If you type + out of habit, OCX accepts it and normalizes it to _. For example, ocx package install kitware/cmake:3.28.1+20260216 installs the tag 3.28.1_20260216. The normalization happens at the earliest boundary, the identifier parser, so all downstream code sees _ only.

A binary can be built multiple ways. Examples are different optimization profiles (pgo, pgo.lto), feature toggles (freethreaded), or size trade-offs (slim). Variants describe how a binary was built, not where it runs. All variants for a given platform execute on the same host. The user chooses the variant, and it is not auto-detected.

Without variant support, publishers create separate packages (python-pgo, python-debug) to represent these build differences. That loses the semantic connection between builds of the same package.

Why not Docker’s tag-suffix convention?

Docker Hub encodes variants as tag suffixes (python:3.12-slim). This creates a parsing ambiguity with semver prereleases, because 3.12-alpha and 3.12-slim are syntactically indistinguishable.

Concourse CI’s registry image resource documents this explicitly. It resorts to a heuristic: only alpha, beta, and rc are treated as prereleases, and everything else is a variant suffix. OCX avoids this ambiguity entirely with a variant-prefix format.

OCX uses a The variant name comes before the version, separated by a hyphen: debug-3.12.5. Because variants start with a letter and versions start with a digit, the boundary is always unambiguous. Variant names match [a-z][a-z0-9.]*, which means lowercase letters, digits, and dots only. convention. The default variant’s tags carry no prefix. For example, astral-sh/python-build-standalone:3.12 resolves the same build as astral-sh/python-build-standalone:pgo.lto-3.12 when pgo.lto is the default.

TagMeaning
astral-sh/python-build-standalone:3.12Default variant at version 3.12
astral-sh/python-build-standalone:debug-3.12Debug variant at version 3.12
astral-sh/python-build-standalone:debug-3Newest version of the debug variant in the 3.x series
astral-sh/python-build-standalone:debugNewest version of the debug variant
astral-sh/python-build-standalone:latestNewest version of the default variant

Rolling tags cascade within their variant track. For example, debug-3.12.5 cascades to debug-3.12, then debug-3, then debug. Variants never cross: publishing a new debug build never updates pgo.lto or the default variant’s tags. See Cascades for the full cascade model.

Aliasing onto the bare track is opt-in, not automatic. ocx package push --default lets the pushed tag’s own variant also own the un-prefixed track. That produces unadorned alias tags alongside its own prefixed ones. With --cascade, the bare 3.12.5 → 3.12 → 3 → latest track cascades too. Without --default, a variant-prefixed push never touches the bare track.

Publishers are expected to maintain the full tag hierarchy. When kitware/cmake:3.28.1_20260216120000 is released, all rolling ancestors are re-pointed. This happens only if the release is genuinely the highest version at each specificity level:

cmake:latest ← rolling — updated
↓
cmake:3 ← rolling — updated
↓
cmake:3.28 ← rolling — updated
↓
cmake:3.28.1 ← rolling — updated
↓
cmake:3.28.1_20260216120000 ← source of truth

Publishing kitware/cmake:3.27.5_20260217 would update kitware/cmake:3.27 but not kitware/cmake:3 or kitware/cmake:latest. The 3.28.x series is still ahead. Rolling tags only advance, never regress.

This is a publishing convention, not a guarantee enforced by the registry. Publishers must maintain the cascade manually, or have OCX do it for them.

Cascades operate within a single variant track. Publishing debug-3.28.1 updates debug-3.28, debug-3, and debug. It never touches the default variant’s tags (3.28, 3, latest) or any other variant’s tags. The --default flag opts a variant-prefixed push into also aliasing the bare track, mirroring the variant-prefixed chain under --cascade. See Variants for details.

Because cascading is a publishing convention rather than a registry-enforced rule, the chain above can also drift silently. A transient registry error partway through a cascade push can leave one alias level un-repointed. For example, kitware/cmake:3 still points at last week’s build even though kitware/cmake:3.28.1 published successfully as a newer version at that level.

Nothing about ordinary resolution notices this. A rolling tag that resolves to some valid manifest looks the same whether or not it is the highest one.

ocx package cascade check closes that gap by re-deriving what every alias tag should point to. It does not trust the push that maybe updated the alias. It folds over every published version. For a given alias and platform, the expected build is the highest version under that alias which publishes the platform at all.

Diffing the registry’s actual tags against that fold finds exactly the aliases a swallowed push error left behind. ocx package cascade repair rewrites the affected alias indexes to match.

The OCI Image Index specification defines a An image index is a top-level OCI manifest. It lists several per-platform sub-manifests under one tag. Clients resolve the entry that matches their platform. format. It allows multiple platform builds to live under a single tag, with the same repository name for all of them.

When you install a package, OCX reads your running system and selects the matching build automatically. It needs no flags, no configuration, and no separate repository per architecture.

SystemDetected as
Linux on x86-64linux/amd64
Linux on ARM (Graviton, Ampere, RPi 4+)linux/arm64
macOS on Inteldarwin/amd64
macOS on Apple Silicondarwin/arm64
Windows on x86-64windows/amd64

If a package publishes a universal build with no platform field in the manifest, OCX uses it as a fallback. This is common for interpreted runtimes and truly portable binaries.

The OCI platform model supports finer-grained descriptors for precise compatibility targeting:

  • variant: CPU sub-architecture. For ARM: v6, v7 (32-bit), v8 (64-bit / arm64). OCX selects the most specific match.
  • os.features: OS capability flags, for example win32k for Windows container isolation modes.
  • features: Reserved for future platform extensions in the OCI spec.

OCX matches all declared fields when selecting among manifest entries. The directed compatibility relation and scoring are documented in Platforms. See the OCI Image Index specification for the complete field reference.

The most direct lock is a digest. kitware/cmake@sha256:abc123… bypasses the local index entirely and identifies an exact binary regardless of what any tag points to. All installed bytes are content-addressed, so every package can be pinned this way. It needs no lockfiles and no registry queries, just the hash.

For most use cases, the local index snapshot already provides the lock. Tags resolve to the digest recorded at last update. That mapping does not change until ocx index update runs. A CI runner that never updates its local index gets the same binary on every run.

For tool authors distributing OCX-powered workflows, there is a more ergonomic option. The local index holds only manifest metadata: small JSON files, with no layer archives and no binaries. It can therefore be shipped inside a GitHub Action, Bazel Rule, or DevContainer Feature.

The result is a two-level lock. The tool version locks the index snapshot, which locks the resolved binary. Users pin the tool and get deterministic builds without managing digests, platform conditionals, or separate lockfiles.

The full pattern lives in Indices. It covers bundled snapshots, OCX_INDEX, the --remote and --offline interaction, and the Dependabot and Renovate flow.

  • Versioning section in the user guide: pick a tag, pin a digest, override the platform
  • Platforms: the canonical platform grammar and compatibility relation
  • Indices: local index snapshots, index.ocx.sh, the bundled index pattern, lock semantics
  • Storage: content-addressed package store, layer dedup