Skip to content

Authoring

You publish to OCX in three layers: build a deterministic archive, attach metadata declaring how OCX should expose its environment, and push the result to any OCI registry. Every step in that chain has its own page in this section. This overview frames the publisher journey end-to-end and tells you which page to open for each decision you face.

TL;DR — Publish a Binary

You have a compiled binary, a Python script, or any other executable, and you want consumers to fetch and run it with ocx package install. The shortest path is three steps: lay the binary out under a bin/ directory, drop a sibling metadata.json that adds bin/ to PATH, then bundle and push.

Lay out the working directory so the bundle contents live under build/ and the metadata sidecar sits next to it. The sidecar is not bundled into the archive — ocx package push uploads it as the OCI manifest config blob, and OCX restores it as a sibling of content/ in the assembled package directory at install time:

  • mytool/workdir (cwd for the commands below)
    • build/bundle contents — becomes content/ after install
      • bin/
        • mytoolyour binary, Python script, shell wrapper, …
    • metadata.jsonOCI config sidecar — not part of the archive

The sidecar declares one public env entry that prepends the package's bin/ to PATH:

json
{
  "$schema": "https://ocx.sh/schemas/metadata/v1.json",
  "type": "bundle",
  "version": 1,
  "env": [
    {
      "key": "PATH",
      "type": "path",
      "value": "${installPath}/bin",
      "visibility": "public"
    }
  ]
}

Bundle build/ into a deterministic archive, then push it to any registry you can authenticate against. The push attaches metadata.json as the manifest config blob — there is no copy inside the archive:

sh
ocx package create build -m metadata.json -o mytool-1.0.0.tar.xz
ocx package push -n -p linux/amd64 -m metadata.json \
  ghcr.io/me/mytool:1.0.0 mytool-1.0.0.tar.xz
Bundle and push mytool:1.0.0

Consumers now reach it with ocx package install ghcr.io/me/mytool:1.0.0 and the binary lands on their PATH. Everything below this section refines that flow: deterministic bundling, dependency edges, env visibility, named launchers, rolling tags, multi-platform manifests, and patterns for wrapping third-party tools you do not own.

Scripted runtimes

If your "binary" is a Python script, a JAR, or anything that needs an interpreter, the script's shebang or wrapper still works — but the cleaner path is to declare the runtime as a dependency and ship a named entry point that resolves the interpreter from that dep. Skip the ambient python3 lookup; pin the version your tool expects.

The Publisher Journey

Most packages start as an upstream binary release on GitHub or a vendor's download page. The work between "I have a binary" and "consumers can ocx package install it" splits into seven decisions:

  • Bundle anatomy — what goes in the archive, whether to repack the upstream layout, which compression to pick. Reach for this when you have a directory and need to turn it into a .tar.xz ready for ocx package push.
  • Declaring dependencies — when to depend on another OCX package instead of bundling its bytes, how to pin by digest, what visibility to choose for the dependency edge. Reach for this when your tool needs to find another tool on disk at runtime.
  • Env surface — which env vars to declare, how to mark each one (private / public / interface), when last-wins matters, how to migrate packages published before the entry-points release. Reach for this when designing the contract between you and consumers — and when retrofitting older metadata.json files.
  • Entry points — when to ship named launchers instead of exposing bin/ on PATH, how to pick non-colliding names, how the launcher resolves each name against the composed PATH from the package's env block. Reach for this when your tool depends on a shared runtime (Python, Node, JVM) and needs its dep graph encapsulated so two installed tools cannot fight over a single ambient interpreter.
  • Building and pushing — first push, --cascade for rolling tags, BYO archives, cross-package layer reuse by digest. Reach for this when ready to publish.
  • Testing locally — verify a package works before pushing: run the same install pipeline in a temp directory, exec a command in the composed env, inspect the layout with --keep. No registry round-trip, no new digest.
  • Multi-platform packages — pushing per platform under one tag, how OCX assembles the OCI Image Index. Reach for this when supporting more than one OS/arch.
  • Migration patternsocx_mirror specs, repackaging Homebrew and GitHub Releases, attaching description metadata. Reach for this when wrapping an upstream tool you do not own.

Decision Flowchart

Common questions you will hit while authoring, and the page that answers each:

QuestionPage
Should I repack the upstream archive or use strip_components?Bundle anatomy → strip components
Which env vars should consumers see vs. only my launchers?Env surface → choosing visibility
When do I need named entry points instead of PATH += bin/?Entry points → when to declare
When do I need a name override on a dependency?Dependencies → name override
How do I verify a package works before pushing it to the registry?Testing locally
How do I make rolling tags (1.0, 1, latest) follow new releases?Building & pushing → cascade
How do I publish for amd64 + arm64 + darwin under one tag?Multi-platform packages
How do I avoid re-uploading a base layer that's already in the registry?Building & pushing → layer reuse
How do I add a README or logo to a published package?Migration patterns → describe
How do I retrofit an older metadata.json for entry visibility?Env surface → migrating

See Also