- docs
- Publishing
- Maintain patch companions
Maintain patch companions
Write, test, publish and freeze a patch descriptor and the companion packages it names.
A maintainer is the person who authors and publishes patch descriptors. The work is a four-step loop, followed by a refresh step for the machines that consume the result. How patches work explains what a companion does once published. Consumers apply it as described in Patch packages for your infrastructure.
Write a descriptor
Section titled “Write a descriptor”Create a JSON file that follows the descriptor schema. The only required fields are version, which must be 1, and rules. Add the optional $schema key to get autocompletion and validation in any taplo- or VS-Code-style editor. The schema is published at https://ocx.sh/schemas/patch/v1.json.
{ "$schema": "https://ocx.sh/schemas/patch/v1.json", "version": 1, "rules": [ { "match": "*", "packages": ["registry.corp.example/infra/ca-bundle:latest"], "required": true } ]}required: true means the companion must be available. If it is not, the exec fails rather than running without the CA bundle. This is the default and the safe choice for security-critical companions like CA bundles.
Use required: false for non-security companions, such as license servers and metrics endpoints, where running without the companion is acceptable.
The match glob selects the packages a rule applies to. How patches work lists the three glob shapes and what each matches.
Test it locally without publishing
Section titled “Test it locally without publishing”ocx patch test composes the descriptor onto a base package in a scratch environment. It touches neither the live registry nor the real $OCX_HOME, so you can verify the descriptor before you publish it.
ocx patch test \ --descriptor ./my-descriptor.json \ java:21Without a trailing command, patch test prints the composed environment so you can inspect which entries the companion contributes. Add -- java -version, or any command, to run it in that environment instead:
ocx patch test \ --descriptor ./my-descriptor.json \ java:21 -- java -versionpatch test composes the surface the base’s consumers see. Add --self to preview the private surface, the one the base’s own launchers see. A companion’s private variables land there:
ocx patch test \ --descriptor ./my-descriptor.json \ --self \ java:21If the companion package is not yet published, supply a local archive instead of pulling it from the registry:
ocx patch test \ --descriptor ./my-descriptor.json \ --companion-archive ./ca-bundle-1.0.tar.xz \ java:21 -- java -version--companion-archive reads the archive’s metadata sidecar, <archive-stem>-metadata.json. That is the name the --metadata flag of ocx package test defaults to. The sidecar must carry an identifier field, because patch test has no -i flag to supply one.
The identifier must match one of the descriptor’s companion entries exactly, in registry, repository and tag. Spell it out in full. A bare identifier with no registry qualifies against your configured default registry, not the [patches] registry. So ca-bundle:1.0 can silently resolve somewhere the descriptor never pointed. A mismatch fails loudly with exit 64 and names the nearest descriptor entry it found.
The base package has no such shortcut. It must always be published and pullable. --companion-archive previews a companion before it exists on the registry, not the package you are patching.
Publish the companion, then the descriptor
Section titled “Publish the companion, then the descriptor”Publish the companion package first. The descriptor only references it by identifier:
ocx package push -p linux/amd64 \ --identifier registry.corp.example/infra/ca-bundle:1.0 \ ca-bundle.tar.xzThen publish the descriptor. Use --global for a descriptor that applies to all packages. Pass a base identifier to create a per-package descriptor instead:
# Global descriptor (applies to all packages):ocx patch publish \ --descriptor ./my-descriptor.json \ --global
# Per-package descriptor (applies to java only):ocx patch publish \ --descriptor ./my-descriptor.json \ java:21Freeze for reproducible builds
Section titled “Freeze for reproducible builds”OCI tags are mutable. The same ca-bundle:latest tag may point to a different digest tomorrow. For production builds that need byte-for-byte reproducibility, write a snapshot:
ocx patch freezeThis resolves every companion and descriptor in use. It writes patches.snapshot.json beside ocx.lock, or beside $OCX_HOME/ocx.lock when run with --global. Companions are pinned per repository:tag, so a descriptor that names one repository at two tags freezes both versions independently.
The file is derived state, so do not edit it by hand. It records a format version. A version this ocx does not read is refused with exit 65, and the message says to re-run ocx patch freeze. Re-freezing is offline and takes no longer than the first run.
ocx patch freeze reads what this machine has recorded, never an active snapshot. When a project’s ocx.toml exists but cannot be read, it fails with exit 78. It does not write a snapshot that lacks that project’s companions.
Point OCX_PATCH_SNAPSHOT at the file to make commands use only the pinned digests:
export OCX_PATCH_SNAPSHOT="/workspace/patches.snapshot.json"With the snapshot in place, every command except ocx patch sync and ocx patch freeze uses only the snapshot’s descriptors and pinned digests. None of them looks up a live tag. ocx package install, ocx package pull, ocx pull and ocx lock write no companion pin and no descriptor state.
A companion the snapshot omits stays out, even when this machine has a pin for it. A required one fails with exit 79. ocx patch sync is refused with exit 78 because it advances pins. To return to floating (live) tags, unset the variable.
A fresh machine may have only the snapshot. There, a pinned descriptor or companion missing from the local store is fetched by its frozen digest, never by tag. Nothing is recorded. Under --offline nothing is fetched. A missing descriptor or companion that is required fails with exit 79.
ocx clean roots the companions a snapshot pins only while OCX_PATCH_SNAPSHOT is set. Without it, clean keeps only recorded pins. A companion that only the snapshot pins is then collected, and the next run fetches it again by digest or fails offline. Keep the variable exported when you run ocx clean. Running ocx patch sync first, with the variable unset, protects those companions only while the live tags still name the frozen digests.
OCX_PATCH_SNAPSHOT is forwarded to child ocx processes, such as generated entrypoint launchers. They resolve the same frozen companion digests as the parent.
Freezing the patch tier is a deliberate opt-in. It is independent of --frozen, which scopes to the package tier. Patches float by design, so a companion resolves live under that flag exactly as it does without it.
Refresh descriptors
Section titled “Refresh descriptors”After the [patches] tier is configured, keep descriptors and companions up to date with:
ocx patch syncpatch sync re-fetches every descriptor for these packages:
- All installed packages.
- The tools locked in every known project’s
ocx.lock. - The global descriptor.
It installs any newly referenced companion packages and re-checks packages installed before the [patches] tier was added. It is safe to run often, because it piggybacks on the same index-update mechanism as ocx index update.
Other commands contact the patch registry less often. These commands re-check each descriptor they have seen before, whether it was found or absent:
ocx package install, ocx package pull, ocx pull, ocx lock, ocx add, ocx update and ocx package test.
They fetch a descriptor again when it changed. ocx exec and ocx env use the cached state and fetch only a descriptor they have never seen.
A failed re-check falls back to the cache, unless the tier is required, where it fails the command. Under a required tier, a descriptor that was present and has since been removed is an error (exit 79). The error stays until ocx patch sync records it as absent.
Without --platform, patch sync resolves companions for every concrete ship platform, not just the platform running the sync. This is the same default ocx lock uses. It is patch sync’s one sanctioned multi-platform fan-out: an explicit enumeration over the concrete matrix, not a selection among candidates.
A synced descriptor and companion set is a shared artifact. If it covered only the maintainer’s own platform, a teammate on a different OS or architecture would miss a required companion. They would hit a failed or unpatched launch. Pass a single --platform to narrow the sync to that platform’s companions.