Skip to main content
Version: v5.7.0

Running OpsChain in CI

Included from Basic tier

A CI job runs OpsChain on the local lane: the binary in the job, the checked-out repository as the configured project, no server. This page is the machine contract that lane offers a pipeline — the exit codes, the run document, the quiet and non-terminal rendering rules, the environment a step sees, the artifact download — and the usage of the two published packagings of it, the GitHub Action and the GitLab CI component. The guided walk is opschain learn 2.8; the flags themselves are opschain docs cli/runs-create-change and opschain docs cli/runs-create-workflow.

The invocation​

opschain runs create change <action> --env <env> --local --approve-all -o json [--ref <git-ref>] [-i k=v ...]
opschain runs create workflow <code | file> --env <env> --local --approve-all -o json [--ref <git-ref>] [-i k=v ...]
FlagRole in a pipeline
--localPins the lane. A runner that carries OPSCHAIN_SERVER or a stored context cannot redirect the job to a server.
--approve-allAnswers every wait: step with its defaults. Without it an unattended run cancels (exit 3) the moment a wait step needs a terminal.
-o jsonPrints one run document on stdout at completion and nothing else; -o yaml is the same document in YAML.
-qPrints only the run id; the exit code is the verdict.
--ref <git-ref>Runs a clean snapshot of the named commit instead of the working copy; the document records it under run.commit.
-i k=vA typed input, validated against the action's or workflow's declarations before the run exists; repeat for lists.
--timeout <d> / --step-timeout <d>Bound the job; expiry exits 4.
--asset <code>Target an asset within the environment.

The two lanes refuse each other's flags by name with exit 2, so a mis-targeted job fails loudly rather than half-working.

Exit codes​

CodeMeaningDocument flags
0The run succeeded.failed: false, cancelled: false, timed_out: false
1The run failed (error, system_error, aborted), or the command itself failed before or after the run.failed: true when a run exists
2Usage error: bad flags, a lane refusal, a malformed k=v, a missing --env.no document
3The run was cancelled: a wait step with no interactive stdin and no --approve-all, or an interrupt (SIGINT).cancelled: true
4The run timed out.timed_out: true

The code is the only thing a pipeline needs to read to pass or fail a job. A step that must keep going after a failure captures the code (set +e … status=$? … set -e) and acts on it.

The run document​

-o json prints exactly one document when the run reaches a terminal status:

{
"run": {
"id": "01a1b365-bc34-731b-992e-469b83b00afa",
"kind": "change",
"action": "deploy",
"target": "myrepo/dev",
"status": "success",
"display_status": "success",
"commit": "eaa9ecfe14a700eb12dd773fb5e540e1281a90f5",
"git_rev": "main",
"created_by": "local",
"created_at": "2026-11-07T00:25:51Z",
"started_at": "2026-11-07T00:25:51Z",
"finished_at": "2026-11-07T00:25:51Z"
},
"steps": [
{"path": "deploy", "name": "deploy", "kind": "action", "status": "success", "children": [
{"path": "deploy/build", "name": "build", "kind": "action", "status": "success"},
{"path": "deploy/release", "name": "release", "kind": "action", "status": "success"}
]}
],
"failed": false,
"cancelled": false,
"timed_out": false
}
  • run is the run summary — the same item opschain runs show -o json and runs list -o json print; commit, git_rev, created_by and name (a workflow's display name) are omitted when empty, started_at / finished_at are null until set, timestamps are RFC 3339 UTC. commit is the revision the run executed: the resolved SHA for a --ref run; for a working-copy run the checkout's HEAD SHA when the tree is clean, and <sha>+dirty.<digest> when the job had already written tracked or untracked files into it — the line that tells a reader whether the run proved the commit or the tree.
  • steps is the step tree in the stable step-view shape (opschain docs cli/runs-show): path, name, kind, status, with id, action, strategy, children omitted when empty, and on a workflow run nested_run_id naming the change a step spawned.
  • failed, cancelled and timed_out carry the exit-code verdict for a reader that only has the document.

The step logs are not in the document and are not on stdout under -o json; read them back with opschain logs <run-id> --local (--step <path> narrows it, --download <file> writes them to a file for the job's own log store). The run is in the job's local history (opschain runs list|show --local) for as long as the opschain home lives.

Rendering rules without a terminal​

Output modestdout carries
-o json / -o yamlthe run document only — no step tree, no transitions, no log lines
-qthe run id only
default, stdout not a terminalone line per step transition (<RFC 3339 time> <step path> <from> -> <to>) and each log line as [<step path>] <line>, in order; no redrawn tree, no colour
default, stdout a terminalthe live tree redrawn in place with log lines above it

Stderr carries first-use notices (a generated master key, a local store moved into place) and the error line of a failing command. A pipeline that captures stdout to a file therefore gets a clean document; a pipeline that wants a readable job log and the document runs the job plain and reads opschain runs show <id> --local -o json afterwards.

A wait: step with stdin that is not an interactive terminal fails with stdin is not an interactive terminal — re-run with --approve-all, or run from a terminal, the run cancels, and the exit code is 3.

The environment a step sees​

A local run executes steps through the host executor; the job's environment does not pass through. A step receives:

  • the host allowlist only: HOME, TMPDIR, TMP, TEMP, LANG, LC_ALL, LC_CTYPE, TZ, USER, LOGNAME and the proxy variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, upper and lower case);
  • PATH (the host's, with resolved tool shims and the engine's own opschain shim ahead of it);
  • the engine-supplied variables — OPSCHAIN_INPUT_<NAME> per input, the step-context and output files, the properties and events files (opschain docs runner-executors, "The execution environment").

A CI variable a step needs travels as an -i input or a property, not as an exported variable. The 2026-11-11 single-node work brings a named-variable passthrough and a secret-env:// reference to the local lane; until then this allowlist is the whole story.

Secrets without a keychain​

Secret inputs and secret-vault:// references resolve through the local secret store at $OPSCHAIN_HOME/secrets/store.age, encrypted with an age master key (opschain docs cli/secret). The key resolves in order:

  1. OPSCHAIN_MASTER_KEY — the identity (AGE-SECRET-KEY-1…) as a masked CI secret exported into the job;
  2. the key file $OPSCHAIN_HOME/secrets/master.key, mode 0600 (looser permissions are refused);
  3. the OS keychain — skipped entirely with the global --no-keychain, which a headless runner should always pass.

Seed the store on stdin, never on the command line:

printf '%s\n' "$DB_PASSWORD" | opschain secret set db/password --no-keychain

Point OPSCHAIN_HOME (or --home) at a directory inside the job workspace so the store, the local keyset and the run history are the job's alone and do not outlive it on a shared runner.

Images are not pulled​

The local lane is offline-first: a step whose image: is not present on the host fails by name (image <ref> is not present locally and image pulling is disabled). There is no pull verb; pull in a job step before the run and let the CI system's layer cache do the rest.

Artifacts​

opschain runs artifacts <run-id> --local
opschain runs artifacts <run-id> --local --download <name> [--out <dir>]

Lists a run's artifacts newest-first, or downloads one by name: a collected step artifact (application/x-tar) unpacks under --out with its workspace-relative paths; any other artifact lands as one file. On the local lane a workflow run's listing includes the artifacts of every change it spawned. -o json on the listing prints the summaries (id, code, run_id, size, content_type, file_count, created_by, created_at) for a loop that downloads them all by code (opschain docs cli/runs-artifacts, opschain docs artifacts).

The GitHub Action​

LimePoint/opschain-action is a composite action that installs the pinned release for the runner's platform, verifies its digest, runs the invocation above, writes the run document to an output and to the job summary, uploads the artifacts, and sets the step result from the exit code:

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: LimePoint/opschain-action@v1
id: run
with:
version: v5.7.0
action: deploy
env: dev
inputs: |
release=${{ github.ref_name }}
dry_run=false
ref: ${{ github.sha }}
download-token: ${{ secrets.OPSCHAIN_RELEASES_TOKEN }}
env:
OPSCHAIN_MASTER_KEY: ${{ secrets.OPSCHAIN_MASTER_KEY }}
- run: echo "run ${{ steps.run.outputs.run-id }} finished ${{ steps.run.outputs.status }}"
InputDefaultMeaning
versionrequiredThe release to install (v5.7.0).
download-token—A GitHub token with read access to the releases repository; releases are invitation-only, so without it the install fails by name. Store it as a repository secret.
action—The action to run; exactly one of action and workflow.
workflow—The workflow code or file to run instead of an action.
envrequiredThe target environment code.
asset—The target asset code.
inputs—Typed inputs, one key=value per line.
ref—A git ref to run a clean snapshot of (${{ github.sha }} pins the job's commit).
approve-alltrueAnswer wait steps with their defaults. false makes a wait step fail the job with exit 3.
artifactstrueDownload the run's artifacts and upload them with actions/upload-artifact.
artifact-nameopschain-artifactsThe name of the uploaded artifact bundle.
home${{ runner.temp }}/opschain-homeThe opschain home for the job.
working-directory.The repository directory the run executes in.
timeout—A run timeout, e.g. 30m.

Outputs: run-id, status, commit, exit-code, run-document (the JSON, single line) and artifacts-dir. The step fails when the exit code is not 0; a job that wants to read the outputs first sets continue-on-error: true on the step and checks exit-code.

The GitLab CI component​

LimePoint/opschain-gitlab-ci publishes two templates with the same inputs, run-change and run-workflow:

include:
- component: gitlab.com/LimePoint/opschain-gitlab-ci/run-change@1.0.0
inputs:
version: v5.7.0
action: deploy
env: dev
inputs: |
release=$CI_COMMIT_REF_NAME
dry_run=false
ref: $CI_COMMIT_SHA
stage: test

The job installs and verifies the binary, runs the invocation, keeps opschain-run/run.json and opschain-run/artifacts/ as job artifacts (always, so a failed run's report is kept too), exports OPSCHAIN_RUN_ID, OPSCHAIN_RUN_STATUS and OPSCHAIN_RUN_COMMIT in a dotenv report for later stages, and fails the job on a non-zero exit code. Set OPSCHAIN_MASTER_KEY as a masked, protected CI/CD variable for the secret store.

What a CI run cannot do​

A CI run is a local run, so everything the local lane refuses by name is refused in CI. The pairs that come up in pipelines:

Refused in CIServer lane
change: steps spawning a child change on another nodethe change runs against a real node (opschain learn 3.1)
content() / list() over the repositorya server change resolves them from its git remote
packs declared with url: / remote:the server fetches and pins the pack; the bundled pack and bundle: files work in CI
workflow retry (runs retry on a workflow run)retry on the server, or re-run the job
a run anyone can cite as evidenceraise the change on a server from the job — the LimePoint/opschain-action reusable workflows opschain-change.yml / opschain-workflow.yml (opschain docs integrations-github, "The published workflows"), or opschain login --server <url> --token <api key> followed by runs create change <node> --action <a> --follow, whose exit codes are the table above
  • opschain docs cli/runs-create-change / opschain docs cli/runs-create-workflow — every flag and named error of the invocation.
  • opschain docs cli/runs-artifacts and opschain docs artifacts — the artifact store and the download.
  • opschain docs cli/secret — the local secret store and its master key.
  • opschain docs runner-executors — the host executor's environment and the offline-first image rule.
  • opschain docs cli/init — scaffolding .opschain/ in a repository.
  • opschain learn 2.8 — the guided walk; opschain learn 0.2 — the two lanes.