Versioning
Every binary package has three dimensions: what it is, which version, and which platform build. Most ecosystems handle these separately — apt with arch suffixes and ports mirrors, pip with platform-encoded wheel filenames, Bazel rules with hand-curated URL matrices — and leave the coordination to the consumer.
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 — without flags, name suffixes, or mirror configuration. This page explains how tags, variants, and platforms compose under the hood. The user-facing surface — pick a tag, pin a digest, override platform — lives in the Versioning section of the user guide.
Tags
An OCI tag is a human-readable label pointing to a specific manifest. The OCI Distribution Specification offers no notion of tag stability — 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 — typically a UTC timestamp like _20260216120000 — 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.
Why _ instead of semver's +?
The OCI Distribution Specification restricts tags to [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} — the + character is not allowed. OCX uses _ as the build separator so every version string is a valid OCI tag by construction. This follows the same convention adopted by Helm for OCI-stored charts.
If you type + out of habit (e.g., kitware/cmake:3.28.1+20260216), OCX accepts it and normalizes to _ automatically. See Build Separator in the FAQ for the full rationale.
| Tag | Publisher's intent | After index refresh |
|---|---|---|
kitware/cmake:3.28.1_20260216120000 | Do not re-push | Same build (by publisher convention) |
kitware/cmake:3.28.1 | Rolling patch | Latest build of 3.28.1 |
kitware/cmake:3.28 | Rolling minor | Latest 3.28.x build |
kitware/cmake:3 | Rolling major | Latest stable 3.x build |
kitware/cmake:latest | Floating | Latest release |
OCI tags are not immutable
Any tag — including build-tagged ones — can be overwritten by the publisher. Two mechanisms protect installs regardless of what the registry does later:
- Local index snapshot. The tag → digest mapping only changes when
ocx index updateruns. The snapshot is yours until you decide to refresh it. - Content-addressed package store. Once a binary is installed, it lives at a path derived from its SHA-256 digest. The tag used to install it is irrelevant — the bytes are permanent.
For absolute reproducibility without any index, reference the digest directly: kitware/cmake@sha256:abc123…
Variants
A binary tool can be built multiple ways — 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, it is not auto-detected.
Without variant support, publishers create separate packages (python-pgo, python-debug) to represent these build differences, which loses the semantic connection between builds of the same tool.
Why not Docker's tag-suffix convention?
Docker Hub encodes variants as tag suffixes (python:3.12-slim), but this creates a parsing ambiguity with semver prereleases — 3.12-alpha and 3.12-slim are syntactically indistinguishable. Concourse CI's registry image resource documents this explicitly, resorting to a heuristic: only alpha, beta, and rc are treated as prereleases, everything else is a variant suffix. OCX avoids this ambiguity entirely with a variant-prefix format.
OCX uses a variant-prefix format convention. The default variant's tags carry no prefix — 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.
| Tag | Meaning |
|---|---|
astral-sh/python-build-standalone:3.12 | Default variant at version 3.12 |
astral-sh/python-build-standalone:debug-3.12 | Debug variant at version 3.12 |
astral-sh/python-build-standalone:debug-3 | Latest version of the debug variant in the 3.x series |
astral-sh/python-build-standalone:debug | Latest version of the debug variant |
astral-sh/python-build-standalone:latest | Latest version of the default variant |
Rolling tags cascade within their variant track: debug-3.12.5 cascades to debug-3.12 → debug-3 → 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.
For the default variant, cascading also produces unadorned alias tags: publishing a new default-variant build cascades both the prefixed track (if any) and the bare 3.12.5 → 3.12 → 3 → latest track.
Platform vs. variant
OCI platform.variant is a CPU sub-architecture field (ARM v6, v7, v8) — it determines where a binary can run and is selected automatically. OCX software variants determine how a binary was built and are selected by the user. The two concepts are orthogonal.
Cascades
Publishers are expected to maintain the full tag hierarchy. When kitware/cmake:3.28.1_20260216120000 is released, all rolling ancestors are re-pointed — but only if this is genuinely the latest 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 truthPublishing 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 — but never touches the default variant's tags (3.28, 3, latest) or any other variant's tags. For the default variant, cascading also produces unadorned alias tags that mirror the variant-prefixed chain. See Variants for details.
Cascading a new release
ocx package push --cascade handles the full cascade automatically: publish one build and let OCX re-point all rolling ancestors in a single command.
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 — 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 — and nothing about ordinary resolution notices, because a rolling tag that resolves to some valid manifest looks the same whether or not it is the latest one.
ocx package cascade check closes that gap by re-deriving what every alias tag should point to — not from the push that maybe updated it, but as a fold over every version currently published: 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.
Cascade the mechanism vs. cascade the command
"Cascade" names two related but distinct things. ocx package push --cascade is the push-time mechanism above — it re-points rolling ancestors as part of publishing one build. ocx package cascade check/repair is a separate, after-the-fact command group: it reads the tag graph a series of pushes left behind and repairs whatever an interrupted push failed to keep consistent. Neither needs the other — check/repair work just as well on a package that was never pushed with --cascade at all.
Platforms
The OCI Image Index specification defines a multi-platform manifest format that allows multiple platform builds to live under a single tag. When you install a package, OCX reads your running system and selects the matching build automatically — no flags, no configuration, no separate repository per architecture.
| System | Detected as |
|---|---|
| Linux on x86-64 | linux/amd64 |
| Linux on ARM (Graviton, Ampere, RPi 4+) | linux/arm64 |
| macOS on Intel | darwin/amd64 |
| macOS on Apple Silicon | darwin/arm64 |
| Windows on x86-64 | windows/amd64 |
If a package publishes a universal build with no platform field in the manifest, OCX uses it as a fallback — common for interpreted runtimes and truly portable binaries.
Richer platform descriptors
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, e.g.win32kfor Windows container isolation modes.features— Reserved for future platform extensions in the OCI spec.
OCX matches all declared fields when selecting among manifest entries, through the directed compatibility relation and scoring documented in Platforms. See the OCI Image Index specification for the complete field reference.
OCI platform.variant ≠ OCX variants
platform.variant is a CPU sub-architecture field, distinct from OCX software variants, which describe build-time characteristics like optimization profiles or feature sets.
Locking
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. Because all installed bytes are content-addressed, every package can be pinned this way — no lockfiles, 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, and 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, no layer archives, no binaries — so it can 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 — bundled snapshots, OCX_INDEX, --remote and --offline interaction, Dependabot/Renovate flow — lives in Indices.
See Also
- Versioning section in the user guide — how-to: pick a tag, pin a digest, override platform
- Platforms — the canonical platform grammar and compatibility relation
- Indices — local index snapshots,
index.ocx.sh, bundled index pattern, lock semantics - Storage — content-addressed package store, layer dedup
- Build Separator FAQ — full rationale for
_over+