Skip to content

Migration Patterns ​

Most upstream projects you will publish to OCX already ship binaries somewhere — a Homebrew formula, a GitHub Release zip, an APT repository, or a vendor's signed installer. The publisher work is reformatting those artefacts into OCI image manifests without re-bundling content. This page covers the recurring migration patterns and points at the ocx_mirror tool that automates them.

The ocx_mirror Pipeline ​

ocx_mirror is the automation backbone every in-tree mirror uses to keep public OCX packages in sync with their upstreams. It reads a YAML spec describing where to find upstream releases, which assets to grab per platform, and how to map them onto OCX metadata, then drives a download → bundle → push pipeline through the same ocx_lib publisher API the ocx package commands use.

The advantage over running the commands manually: the pipeline batches --cascade resolution across platforms in one publisher invocation, runs the upstream → archive transformation deterministically, and exposes a versions.new_per_run knob (Option<usize>, unlimited when omitted) so a single CI run doesn't spam the registry with a backlog of upstream releases. The cross-release layer-reuse pattern is a hand-publisher technique — ocx_mirror always uploads each platform's archive afresh.

A minimal spec that wraps a single upstream project:

yaml
name: mytool
target:
  registry: ocx.sh
  repository: mytool

source:
  type: github_release
  owner: example-org
  repo: mytool
  tag_pattern: "^v(?P<version>\\d+\\.\\d+\\.\\d+)$"

assets:
  linux/amd64:
    - "mytool-.*-linux-x86_64\\.tar\\.gz"
  linux/arm64:
    - "mytool-.*-linux-aarch64\\.tar\\.gz"

asset_type:
  type: archive
  strip_components: 1

metadata:
  default: metadata.json

cascade defaults to true; set cascade: false only for repositories that should not advance rolling tags. A non-trivial worked example covers the multi-platform asset matrix, version-range filtering, and per-platform metadata overrides for a single upstream:

yaml
name: cmake
target:
  registry: ocx.sh
  repository: cmake

source:
  type: github_release
  owner: Kitware
  repo: CMake
  tag_pattern: "^v(?P<version>\\d+\\.\\d+\\.\\d+)$"

# Multi-platform asset matrix: each platform lists one or more regexes;
# the union must resolve to exactly one asset filename per release.
assets:
  linux/amd64:
    - "cmake-.*-linux-x86_64\\.tar\\.gz"      # >= 3.20.0
    - "cmake-.*-Linux-x86_64\\.tar\\.gz"      # < 3.20.0
  darwin/arm64:
    - "cmake-.*-macos-universal\\.tar\\.gz"   # universal fat binary
  windows/amd64:
    - "cmake-.*-windows-x86_64\\.zip"         # >= 3.20.0
    - "cmake-.*-win64-x64\\.zip"              # < 3.20.0

asset_type:
  type: archive
  strip_components: 1

# Per-platform metadata overrides for a non-trivial upstream.
metadata:
  default: metadata.json
  platforms:
    darwin/arm64: metadata-darwin.json
    windows/amd64: metadata-windows.json

# Version-range filtering: only mirror releases >= 3.31.0, max 10 per run.
versions:
  min: "3.31.0"
  new_per_run: 10

skip_prereleases: true
cascade: true

Repackaging GitHub Releases ​

Most modern open-source binaries ship via GitHub Releases. The mirror pipeline's github_release source type reads the release feed, downloads the assets matching the platform regex, and pushes them through the same ocx_lib publisher API used by ocx package create / push. Hand-driven publishers can mimic the same flow with a few curl calls and a script — the wins from going through ocx_mirror are deterministic ordering across runs and the dedicated digest-verification slot (verify.github_asset_digest).

The platform regex matrix is the part publishers usually re-derive every time. Two CMake-style worked examples covering the asset-naming-changed-mid-release case:

yaml
linux/amd64:
  - "cmake-.*-linux-x86_64\\.tar\\.gz"      # >= 3.20.0
  - "cmake-.*-Linux-x86_64\\.tar\\.gz"      # < 3.20.0

The pipeline applies every regex and requires the union of matches to resolve to exactly one asset filename per release; ambiguous matches abort with Ambiguous. Two regexes that match the same upstream filename are fine; two regexes that match different filenames are an error. See the CMake worked example above for the full matrix.

Repackaging a release asset by hand ​

A hand-driven publisher who downloads one release asset directly, without going through ocx_mirror, still has to solve the same problem ocx_mirror solves internally: a GitHub Release tarball almost always wraps its content in a version-named directory (mytool-1.2.3-linux-x86_64/bin/mytool), and OCX bundles need a flat surface at their root. Unpacking that archive by hand, stripping the wrapper, and re-tarring it just to feed ocx package create is exactly the manual step --extract exists to remove.

sh
curl -LO https://github.com/example-org/mytool/releases/download/v1.2.3/mytool-1.2.3-linux-x86_64.tar.gz

# Treat the downloaded release asset itself as the archive to bundle, stripping
# its single wrapper directory on the way in.
ocx package create mytool-1.2.3-linux-x86_64.tar.gz \
  --extract --strip-components 1 \
  -i mytool:1.2.3 -p linux/amd64 \
  -o mytool-1.2.3-linux-amd64.tar.xz

--extract unpacks the downloaded asset into a temporary directory and bundles that tree instead of the archive file — the metadata sidecar, the binaries scan, and the libc check all run against the extracted content exactly as they would against a directory you unpacked yourself. --strip-components runs once here, against the upstream asset at authoring time; it is a different mechanism from the metadata strip_components field a mirror spec declares, which instead re-applies on every consumer's install — see Two strip_components, two different moments for how the two relate.

Repackaging Homebrew Formulae ​

Homebrew formulae are Ruby DSL programs, not declarative manifests — every formula encodes its own download URL, build steps, and install hook. There is no general-purpose "import a Homebrew formula" path. The migration pattern is to read the formula's bottle do … root_url … sha256 … block (for binary bottles) or the formula's url + sha256 (for source releases), mirror those bytes into a github_release-shaped pipeline (or fetch directly), and write OCX metadata.json covering the env entries Homebrew would have set in its post-install script.

Binary Homebrew bottles unpack as <formula>/<version>/bin/..., so strip_components: 2 matches the typical layout. Pair that with a PATH env entry marked "visibility": "public" (the default is private) so consumers actually see the binaries on PATH. Source-built Homebrew formulae do not migrate cleanly without rebuilding, since OCX is a binary package manager.

Attaching Description Metadata ​

Once the package is in the registry, publishers attach a README, logo, title, search keywords, and short description via ocx package description push. The fields land as an OCI image manifest pushed under the dedicated internal tag __ocx.desc, so the description travels with every registry export and powers the package catalog. The round-trip is cheap to verify: ocx package description pull reads the same fields back, with --save-readme writing the published markdown to disk for diffing.

sh
ocx package description push \
  --readme README.md \
  --logo logo.svg \
  --title "mytool" \
  --description "A small example tool" \
  --keywords cli,linting \
  mytool
ocx package description pull acme/mytool
Attaching package descriptions and reading them back

Re-publish runs do not auto-refresh descriptions

The ocx_mirror pipeline pushes packages, not descriptions — ocx package description push is a separate hand-driven step. Re-run it whenever your README or logo changes; otherwise the registry keeps the previously published __ocx.desc manifest.

See Also ​