Skip to content
ocx
install

Use ocx in GitHub Actions

This page shows how to make an ocx toolchain available to every step of a GitHub Actions job. It covers the setup-ocx action, the plain ocx env --ci route, and the pull flow for runners that share a cache.

Shell environment changes do not cross step boundaries. An eval "$(ocx env --shell=bash)" lives only in the current shell, so a package set up in step one is invisible in step two.

ocx env --ci and ocx package env --ci write the composed environment into the CI system’s own persistence channel instead of printing export lines. Later steps see the full environment without glue code.

GitHub Actions runners provide two file-based channels for sharing state across steps:

  • $GITHUB_PATH: each appended line is prepended to PATH for all later steps. OCX writes PATH entries here, so OCX-composed package directories land leftmost, which is the highest priority.
  • $GITHUB_ENV: each KEY=VALUE line (or heredoc block for multiline values) is exported to all later steps.

ocx env --ci=github reads the paths of those files from the runner’s own GITHUB_PATH and GITHUB_ENV variables. It appends the resolved package paths and variables directly, with no jq and no redirect. The command is silent on success, so the files are the only evidence.

Only the literal PATH variable goes to $GITHUB_PATH. Every other path-type variable goes to $GITHUB_ENV as KEY=value, with OCX-provided directories prepended to the existing value. Examples are LD_LIBRARY_PATH, MANPATH and PKG_CONFIG_PATH.

A list-type variable such as JDK_JAVA_OPTIONS also goes to $GITHUB_ENV. It folds the opposite way: OCX’s contribution is appended to the existing value, joined by the key’s separator.

Outside a GitHub Actions runner, GITHUB_ENV and GITHUB_PATH are unset and --ci=github exits 78 (configuration error). Bare --ci with no provider and no detected CI exits 64.

The turnkey path is the ocx-sh/setup-ocx action. In project mode it installs OCX and pulls the project toolchain from ocx.lock. It then replays ocx env --ci=github into $GITHUB_PATH and $GITHUB_ENV, with a build cache keyed on the hash of ocx.lock.

The example needs ocx.toml and ocx.lock committed to the repository, with kitware/cmake and shellcheck declared as tools. Your first project creates both files.

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ocx-sh/setup-ocx@v1
- name: Build
run: cmake --version && shellcheck --version

After the setup-ocx step, every later step sees the project’s resolved package directories in PATH and any declared environment variables.

The example above uses the v1 tag for readability. GitHub recommends pinning third-party actions by commit SHA. Resolve the SHA of a release tag yourself and keep the tag as a comment:

Terminal window
git ls-remote https://github.com/ocx-sh/setup-ocx 'refs/tags/v1*'

The line below pins v1.4.0, the release current when this page was written.

- uses: ocx-sh/setup-ocx@25fa771f8572572dc64528db89560de68a163a0e # v1.4.0
Without the action: install OCX manually

To avoid the setup-ocx action, install OCX with the POSIX installer and replay the toolchain yourself. The installer puts the ocx binary under ~/.ocx/symlinks/ocx.sh/ocx/cli/current/content/bin. Add that directory to $GITHUB_PATH so ocx resolves in later steps.

For a reproducible runner, install a pinned version. The version-pinned installer URL and ocx self setup <tag> both give every run the same build.

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install OCX
run: |
curl -fsSL https://setup.ocx.sh/sh | sh
echo "$HOME/.ocx/symlinks/ocx.sh/ocx/cli/current/content/bin" >> "$GITHUB_PATH"
- name: Set up toolchain
run: ocx env --ci=github
- name: Build
run: cmake --version && shellcheck --version

Individual OCI packages have no ocx.lock to pull. Install OCX with the action (install only) or the POSIX installer, then resolve each package with ocx package env:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install OCX
run: |
curl -fsSL https://setup.ocx.sh/sh | sh
echo "$HOME/.ocx/symlinks/ocx.sh/ocx/cli/current/content/bin" >> "$GITHUB_PATH"
- name: Resolve package environment
run: ocx package env --ci=github ocx.sh/kitware/cmake:3.28 ocx.sh/shellcheck/shellcheck:0.11
- name: Test
run: cmake --version && shellcheck --version

An identifier needs its namespace. A bare node:20 fails with exit 79 because the name is not in the index.

CI needs package binaries available with their environment variables exported. It does not need version switching, candidate symlinks or the rest of the install-store machinery that supports interactive use.

For project-toolchain CI, with ocx.toml and ocx.lock committed (see your first project), the recommended flow is:

Terminal window
ocx lock --check
ocx --frozen pull
ocx exec -- cmake --version

ocx lock --check fails the job when the lock is stale, and ocx --frozen pull downloads only the locked digests. Freeze what a job resolves explains both switches.

For OCI-tier CI, with no ocx.toml and direct identifier pinning:

Terminal window
ocx package pull "kitware/cmake:4.2.0"
ocx package env "kitware/cmake:4.2.0"

ocx pull (project tier) and ocx package pull (OCI tier) download packages into the content-addressed package store without creating any symlinks. To export the result into the runner’s files, use ocx env --ci=github as shown above.

OCX_JOBS caps how many root packages are pulled in parallel. It applies to every command that fans out through the package manager: install, pull, package pull, exec and env.

0 means all logical cores, matching GNU parallel -j 0. It differs from Cargo’s --jobs 0, which errors. A negative or non-numeric value is ignored with a warning, and unset is unbounded. The --jobs option takes precedence over the variable.

Terminal window
export OCX_JOBS=4

A step’s environment does not survive to the next step. Exporting PATH in one run: block and expecting the next block to see it is the usual way a working toolchain looks broken on a runner.

ocx env --ci=github solves this. When you want only the launcher directory on PATH, append it to $GITHUB_PATH yourself. This recipe needs jq on the runner, and ocx on PATH from an earlier install or setup-ocx step. Render first, then append:

- run: ocx pull
- run: ocx --format json shell state | jq -r .toolchain_bin >> "$GITHUB_PATH"
- run: cmake --version # a later step: the toolchain is on PATH

The order is the whole recipe. ocx pull writes the launchers and the render stamp, and the append then publishes a directory that has something in it. Reversed, the append publishes an empty directory and the third step cannot find a launcher that was never rendered.

$GITHUB_PATH accepts any string and never checks it. A wrong directory costs a green step and a failure in the next job. That is why the recipe reads the toolchain_bin field instead of joining a path: .toolchain_home + "/bin" looks right, appends cleanly and names nothing.