Skip to main content
Version: v5.7.0

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-common as its remote URL, rev bundled, 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​

ActionPurposeKey inputs
script:executeRun 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.

ActionPurposeKey inputs
terraform:applyThe 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:planThe 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:destroyTear down the workspace's resources; the script never prompts — gate it with a wait/approval step at the call site.workspace, var_file.
terraform:outputPublish terraform output -json as structured step outputs, one per top-level terraform output, sensitive ones sensitive.workspace.

ansible — run committed playbooks​

ActionPurposeKey inputs
ansible:playbookRun 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.

ActionPurposeKey inputs
github:workflow_runDispatch 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_dispatchFire 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.

ActionPurposeKey inputs
buildkit:buildBuild 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:ensureMake 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.

ActionPurposeKey inputs
packer:buildpacker 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:ensureMake 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:common and the baseline union: entitlements.
  • The image build providers and registries the buildkit and packer verbs build against, and the kind: image artifact they record: image-builds, artifacts.