Skip to content
ocx
install

Run ocx unattended in a pipeline

This page shows how to run ocx in a pipeline with no human present. It names the settings a job should make, what each one prevents, and the checks that fail a job instead of letting it drift.

ocx never stops to ask a question in a pipeline. The few things that can differ between your laptop and a runner are background checks, shell-activation consent and version discovery. The sections below take them in turn.

Most CI systems already set CI, which turns off the background checks. The other variables are yours to set, in the job or the image.

VariableSet it toWhat it settles
CIany truthy valueTurns off the self-update notice, the toolchain drift check and the managed-config refresh tick. Most CI systems set it.
OCX_NO_UPDATE_CHECK1The same kill switch, for a runner that does not set CI.
OCX_NO_CONSENT1Writes no shell-activation consent stamp. See Consent.
OCX_FROZEN1An unknown tag errors instead of resolving fresh. See Freeze.
OCX_OFFLINE1No network at all. Use it for the run after a pull.
OCX_JOBSa numberCaps parallel pulls on a small runner.
OCX_NO_CONFIG1Skips the user, $OCX_HOME and managed config tiers. A system-scope policy in /etc/ocx/config.toml still loads, cut to its locked sections.

OCX_NO_CONFIG is env-only and has no flag. The hermetic pattern is OCX_NO_CONFIG=1 ocx --config /ci/ocx.toml ..., which loads the one explicit file plus any system-scope policy.

When ocx cannot install into your profile, as on a pipeline image you build once, pass --no-modify-path to ocx self setup. The installer then writes no profile block and no session PATH entry, and you add the directories yourself. Its options list the rest.

A consent stamp is what lets a shell hook apply a project’s tools at the next prompt. add, remove, lock, update, upgrade, pull, exec and init write one as a side effect. ocx asks nothing: the stamp is a record, not a prompt.

A pipeline has no prompt hook, so a stamp does nothing for it. It would only record consent for a checkout the pipeline’s operator never chose.

Set OCX_NO_CONSENT=1 in the job or the image, never in an interactive profile. Exported globally, it leaves a project inert once its lock grows past the stamped sources. The flag form is --no-consent, and either flag outranks the variable. The variable is forwarded to child ocx processes, so a script run by ocx exec that calls ocx pull stays consistent.

ocx shell allow ignores OCX_NO_CONSENT. It is the explicit grant, so use it where you want one. Shell integration explains stamps and grants.

A pipeline should fail when its inputs move. ocx does most of that by default and gives you two switches for the rest.

The lock is checked for you. ocx pull and ocx exec refuse a stale lock with exit 65. ocx lock --check runs the same test with no network and no write. It exits 65 when ocx.lock does not match ocx.toml and 78 when the lock is missing. Run it first in a job to fail fast.

--frozen closes version discovery. ocx --frozen pull resolves a tag only from the local index or the lock. An unknown tag exits 81. Digest-pinned content still downloads, so frozen is not a network ban.

--offline closes the network. --offline resolves from the local index and the package store only. With both flags, offline wins.

A job that pulls once and then runs offline looks like this:

Terminal window
ocx lock --check # exit 65 if ocx.toml and ocx.lock disagree
ocx --frozen pull # download the locked digests, discover nothing new
ocx --offline exec -- cmake --version

To make the next run cheap, cache the three directories under $OCX_HOME that make a copy offline-capable: blobs, layers and index. Key the cache on the hash of ocx.lock, as the setup-ocx action does. Work offline covers what has to travel for a copied home to work with no network.

A runner has no keychain and no TTY. Set OCX_AUTH_<REGISTRY>_* variables from the CI system’s secret store, or pipe a token to ocx login --password-stdin. Registry authentication recipes give both for GHCR and ECR.

Signing and attesting also look for a browser by default. Pass --no-tty to ocx package sign and ocx package attest so a missing identity token fails the step instead of waiting for a login. Publisher CI shows the OIDC wiring.

A retry wrapper cares about four exit codes. Read the exit code first, then the error document if you ran with --format json. Scripts and JSON shows how.

ExitNameMeaning for a pipeline
75TempFailA rerun may succeed, for example a registry blip.
69UnavailableA rerun will not help until the service returns.
80AuthErrorThe credential is missing or refused. Fix the secret.
81PolicyBlockedA local rule refused the run, such as --frozen, --offline or a trust policy.

The exit code reference lists the rest, with the error slug each one carries.

Beyond the persistence channels of GitHub Actions and GitLab CI, ocx reads a small set of variables the CI system provides. They are the runner’s own, carry no OCX_ prefix and are not ocx configuration.

None is forwarded to child ocx processes. The forwarding set carries only OCX_* variables that affect resolution. The runner’s sink files are runner-managed paths and must not propagate through forked environments.

VariableProviderRead for
GITHUB_ACTIONSGitHub ActionsProvider auto-detection for bare --ci and --ci-annotations
GITHUB_PATH, GITHUB_ENVGitHub ActionsThe sinks ocx env --ci=github writes
GITHUB_SERVER_URL, GITHUB_REPOSITORYGitHub Actionsorg.opencontainers.image.source under --ci-annotations=github, joined with the trailing slash trimmed. If either is absent or blank, no .source annotation is written, never a truncated one
GITHUB_SHAGitHub Actionsorg.opencontainers.image.revision, stamped verbatim
GITHUB_RUN_ID, GITHUB_ACTOR, GITHUB_ACTOR_IDGitHub Actionsocx package announce joins the run id with the server URL and repository into the run’s URL. ocx package claim takes the actor and id as the owner’s login and numeric id when no --owner is given. The forge confirms neither
GITLAB_CIGitLab CI/CDProvider auto-detection
CI_PROJECT_URLGitLab CI/CDorg.opencontainers.image.source under --ci-annotations=gitlab, verbatim. Absent or blank writes none
CI_COMMIT_SHAGitLab CI/CDorg.opencontainers.image.revision, verbatim
CI_PIPELINE_CREATED_ATGitLab CI/CDorg.opencontainers.image.created, unless SOURCE_DATE_EPOCH is set
CI_JOB_TOKEN, CI_JOB_URL, CI_PROJECT_PATHGitLab CI/CDThe credential of ocx package announce --transport git inside a job (a secret), the URL of the run making the announce, and the group/project path of the job’s project
GITLAB_USER_LOGIN, GITLAB_USER_IDGitLab CI/CDThe claiming owner for ocx package claim when no --owner is given. The forge confirms neither
SOURCE_DATE_EPOCHReproducible Buildscreated under --ci-annotations on either provider, outranking CI_PIPELINE_CREATED_AT, so a reproducible pipeline’s push and its attestations agree on one instant
ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, SIGSTORE_ID_TOKEN, CIRCLE_OIDC_TOKEN_V2GitHub Actions, GitLab CI/CD, CircleCIThe ambient OIDC token for keyless ocx package sign and ocx package attest; see Signing from CI

CI_PIPELINE_CREATED_AT is parsed as RFC 3339 and re-emitted in UTC seconds with a literal Z. That is the spelling every other created value uses, so a sub-second or non-UTC input is normalized, not passed through. A value that fails to parse is not stamped: ocx warns on stderr and falls back to the wall clock.

GitHub Actions exposes no equivalent, so --ci-annotations=github uses the wall clock unless SOURCE_DATE_EPOCH is set. The annotation flow is described under Stamping from CI.

ACTIONS_ID_TOKEN_REQUEST_TOKEN and the other tokens are secrets. ocx treats them as secrets. A tool run under ocx exec still inherits the OIDC pair, so that the tool can sign keylessly itself.