The common action pack (packs:common)
packs:common
opschain-pack-common is the baseline action pack, included in every tier from Basic up: a LimePoint-maintained
monorepo of general-purpose YAML actions — run a committed script, drive a
terraform workspace, run an ansible playbook, dispatch a GitHub Actions
workflow, build a container or machine image against a registered build
provider — and a sha-pinned snapshot of it ships inside the binary as a
built-in pack source. Nothing is installed and nothing is fetched: a
.opschain/packs.yaml declaration that names no remote: or url: (or says
bundled: true) resolves against the embedded snapshot — zero setup, fully
offline, the vendored commit as the tested-together floor. The
packs:common entitlement is baseline-unioned: granted on every licence
tier, whatever the applied documents carry, so applying an enterprise licence
can never silently revoke the bundled pack (the baseline-union paragraph of
entitlements). The pack carries a LimePoint-signed pack
manifest at its repository root declaring no required entitlement
(requires empty — it ships in every tier), so no run-creation door
ever gates it.
How the bundle resolves
- A bundle-resolved pack pins with the recognisable marker
bundled://opschain-pack-commonas its remote URL, revbundled, and the vendored commit as its sha — the step preamble and the stored pin both say where the content came from. - A bundled declaration carries no
rev:— the snapshot has exactly one commit, pinned by the release, and declaring one is a named error telling you to remove it or switch to a git source. - After a server upgrade, a pin taken against an older release's snapshot is a named error telling you to refetch the template version (or recreate the change) so it re-pins against this release — never silently different content.
- An explicit
remote:/url:declaration overrides the bundle entirely, for newer pack content between releases; the same monorepo also mirrors onto an internal git host for estates that want to track it directly.
Declaration mechanics, calling conventions (uses: script:execute), the
development loop and the failure modes are the hands-on runbook's territory:
opschain docs getting-started/2-4-using-packs.
The action catalog
Six families, one subdirectory each, every action a typed-input YAML action
executing the pack's own helper scripts (${{ pack.dir }}).
script — the lowest common building block
| Action | Purpose | Key inputs |
|---|---|---|
script:execute | Run a script committed in the calling repository's workspace. | path (required, workspace-relative), args (one shell-tokenised argument string), shell (interpreter override; empty = the script's shebang, then bash). |
Captures both streams by default and publishes stdout, stderr and
exit_status as step outputs, so later steps consume the script's output
without re-running it.
terraform — plan / apply / destroy / output
All four share workspace (required), dir (terraform root, default .)
and backend (default properties: state rides an opschain.files
property entry, conflict-guarded, materialised onto any runner before the
step starts; external leaves state to the repository's own backend block).
All but output also take state_layer (default environment) and, where
relevant, var_file.
| Action | Purpose | Key inputs |
|---|---|---|
terraform:apply | The default posture: plan-and-apply in one step — no handoff. Pass plan_id from a preceding terraform:plan to apply that reviewed planfile instead (the per-change planfile property is applied and then removed). | workspace, plan_id (empty = plan-and-apply), var_file (only when plan_id is empty). |
terraform:plan | The gated-flow half (plan → review/wait → apply): plans to a per-change planfile property and exports its key as the plan_id output. | workspace, var_file. |
terraform:destroy | Tear down the workspace's resources; the script never prompts — gate it with a wait/approval step at the call site. | workspace, var_file. |
terraform:output | Publish terraform output -json as structured step outputs, one per top-level terraform output, sensitive ones sensitive. | workspace. |
ansible — run committed playbooks
| Action | Purpose | Key inputs |
|---|---|---|
ansible:playbook | Run a playbook committed in the calling repository: colourless, promptless, fail-loud ansible-playbook. | playbook (required, workspace-relative), inventory (path or inline host list), extra_vars, limit, tags, check (any non-empty value = --check dry-run). |
github — dispatch and await GitHub Actions runs
Both actions forward the change identity (project, environment, asset,
action, change id) from the step context automatically, find the triggered
run by the change id the target workflow embeds in its run-name, stream
the watch into the step log, and fail loud unless the run concludes
success. Both export run_id and run_url. GH_TOKEN reaches the step
through the opschain.env properties — preferably a github-app://
reference the server redeems as a short-lived just-in-time installation
token at dispatch (opschain docs integrations-github), else a
vault-held PAT.
| Action | Purpose | Key inputs |
|---|---|---|
github:workflow_run | Dispatch a workflow_dispatch workflow and await its conclusion. | workflow (required, file name in the target repo), repo (required, owner/repo), ref (default main), commit_sha, forward_initiated_by ("true" forwards the OpsChain actor as the initiated_by input — the target must declare it). |
github:repo_dispatch | Fire a repository_dispatch event and await the triggered dispatch.yml run. | event (required, the dispatch event type), repo (required), commit_sha. |
buildkit — build container images against a BuildKit provider
The verb for an image build provider of kind buildkit
(opschain docs image-builds): the step receives the provider's contract
(IMAGE_BUILD_PROVIDER, BUILDKIT_HOST, the mTLS client certificate at
IMAGE_BUILD_CREDENTIAL_FILE) and the registry's (IMAGE_REGISTRY_*, the
login at IMAGE_REGISTRY_CONFIG_FILE with its directory as
DOCKER_CONFIG), builds, pushes, and writes the image descriptor a
kind: image artifact declared at the call site records. Every input
rides the input environment, so build arguments keep their quotes.
| Action | Purpose | Key inputs |
|---|---|---|
buildkit:build | Build a Dockerfile in the run workspace with BuildKit, push it, record it. Tool precedence: buildctl on PATH (or the runner cache, or a sha256-verified download from a configured URL), else docker with the buildx plugin through a remote builder on the same daemon; jq is required. | image (required: the name beneath the registry's repository prefix), provider / registry (image-build:// / image-registry:// references; empty = the node's opschain.image.* property, else the deepest binding), context (default .), dockerfile (empty = <context>/Dockerfile), tags (comma-separated; the first is the identifier), build_args (a JSON object or KEY=VALUE lines), platforms, target, push (empty = true when a registry resolved), sbom (true attaches a BuildKit SBOM attestation and, with a declared sbom: file, writes it beside the record). |
buildkit:ensure | Make a BuildKit client available on this runner; build resolves the tool inline the same way (a step may land on any runner of the group). A public download URL is never a default. | source (auto / path / cache / url), version, url + sha256. |
Outputs: image_ref, digest (sha256:…), descriptor, tool.
Refusals by name: no build provider resolved; a provider that is not of
kind buildkit; push requested with no registry resolved; a missing
dockerfile or context; no tag; no BuildKit client; no jq.
The worked example — an application's own build action, with the registry chosen by the node's properties within the administrator's bindings and the result recorded as an image artifact with its SBOM:
# .opschain/actions/web/build_image.yaml
action: build_image
description: Build the web service container image from this commit and record it as an image artifact.
inputs:
tag: { default: "${{ node.code }}-${{ run.id }}" }
provider: { default: "${{ properties.opschain.image.build_provider }}" } # a reference (image-build://…)
registry: { default: "${{ properties.opschain.image.registry }}" } # image-registry://…, likewise
steps:
- name: build
uses: buildkit:build
with:
provider: ${{ inputs.provider }} # the dispatcher swaps each reference for the backend's contract
registry: ${{ inputs.registry }}
image: web
context: .
dockerfile: deploy/Dockerfile
tags: ${{ inputs.tag }},latest # the registry record supplies host + repository; the step names the tags
sbom: "true"
artifacts:
- name: web_image
kind: image # the engine attaches the declaration to the verb's build step
sbom: sbom/web.spdx.json # the verb writes the SBOM here; the record carries it
required: true
The record carries harbor.limebank.internal/web/web:web-42 as its
identifier, the sha256: digest BuildKit reported, the provider and
registry codes and the sealed commit; a deploy change names it as
artifact://…/web_image@sha256:… through artifacts:resolve_image
(opschain docs artifacts).
packer — build machine images against a packer provider
The verb for an image build provider of kind packer: the provider's
credential reaches the step in the form the template's builder reads (an
Azure service principal as AZURE_CLIENT_ID / AZURE_TENANT_ID with
AZURE_CLIENT_CERTIFICATE_PATH or AZURE_CLIENT_SECRET, an AWS session,
a Google access token), so the template authenticates with nothing on its
command line.
| Action | Purpose | Key inputs |
|---|---|---|
packer:build | packer init + packer build on a template committed in the run workspace; read the artefact id from the template's manifest post-processor (custom_data.image_id first, then the builder's artifact_id) and write the descriptor with digest = id:<artefact id>, since a machine-image backend reports no content digest. jq is required. | template (required, workspace-relative), provider / registry (a gallery or ami record), var_file, vars (a JSON object — a list value is passed as its JSON text — or KEY=VALUE lines, each one -var), only, except, manifest (default packer-manifest.json), init (default true). |
packer:ensure | Make packer available on this runner (PATH, the runner cache, or a sha256-verified download from a configured URL); build resolves it inline the same way. | source, version, url + sha256. |
Outputs: image_id, digest (id:…), descriptor. Refusals by name:
no build provider resolved; a provider that is not of kind packer; a
missing template or var file; no manifest written (the message names the
post-processor to add); a manifest naming no artefact id; no packer;
no jq.
steps:
- name: build
uses: packer:build
with:
provider: ${{ inputs.provider }} # image-build://hosted-packer (kind packer)
registry: ${{ inputs.registry }} # image-registry://hosted-gallery (kind gallery)
template: deploy/vm/image/hosted.pkr.hcl
vars: '{"image_version":"${{ inputs.image_version }}","replica_regions":["australiaeast","australiasoutheast"]}'
artifacts:
- name: hosted_appliance
kind: image # identifier = the gallery version id, digest id:<that id>
required: true
Both image-build verbs write the descriptor where the call site's
declaration expects it (OPSCHAIN_IMAGE_DESCRIPTOR, and the SBOM at
OPSCHAIN_IMAGE_SBOM), so the collector records what the verb built; the
descriptor / sbom_file inputs override the paths for a verb called
without a declaration.
Where the core documentation lives
- Mounting, calling and building packs: the action-packs runbook,
opschain docs getting-started/2-4-using-packs. - The signed pack store for commercially distributed packs (the bundled snapshot never needs it): the pack store.
packs:commonand the baseline union: entitlements.- The image build providers and registries the
buildkitandpackerverbs build against, and thekind: imageartifact they record: image-builds, artifacts.