Skip to main content
Version: v5.7.0

2.4 Using packs

Included from Basic tier

After this you can: mount a maintained pack, call one of its actions by name, and browse the pack store from the operator's seat.

Before you start — tick each one:

  • Lesson 2.2 — Namespaces, composition and inputs (opschain learn 2.2)

In the GUI: the Packs panel in the Operations area — the installed pack store: signed bundles, versions and pins.

You do not have to write every wrapper yourself. An action pack is a repository of reusable actions — run a committed script, a terraform plan, an ansible playbook, a container or machine image build — that your repositories mount by name and call like any other action. In this lesson you mount the common pack that ships with the product, call script:execute from it in a local run, and read what the operator sees in the pack store.

1. The pack model​

A repository opts in with .opschain/packs.yaml at its root:

packs:
- name: script # the namespace the pack's actions mount under
path: script # the pack's root inside a monorepo (optional)
# no remote/url: resolves from the opschain-pack-common copy BUNDLED
# with this release — zero setup, works air-gapped, pinned to the
# bundled sha. Declare a remote: or url: to use a newer copy instead:
# remote: opschain-pack-common # a git remote configured on the install
# rev: main # branch, tag or sha — production pins a tag or sha

opschain-pack-common ships with the product. A declaration with no remote: or url: (or an explicit bundled: true) resolves from the snapshot vendored into this release — the version tested with your binary, usable with zero configuration, offline, and in --local runs. The pin records bundled@<sha> and the step preamble says so. A bundled entry may not carry rev: (the bundle has exactly one sha — the error names it); declaring an explicit remote:/url: always overrides the bundle when you want newer pack content between releases. The common pack is granted on every tier — packs:common is always granted by licence resolution, so applying an enterprise licence can never revoke it — and packs you write yourself are never licence-gated; only a commercially licensed LimePoint pack refuses without its packs:<name> entitlement (opschain docs entitlements).

Four ideas carry the whole model:

  • name: is the mount point. The pack's actions appear in your catalog under it: script:execute. Pick a name that does not collide with one of your own action namespaces — a collision is a named error.

  • remote: is an indirection, not a URL. It names a git remote configured on the install, so the same repository works unchanged on a connected site (remote → github.com) and an airgapped one (remote → the internal mirror). For quick development there is also an inline url: form — declare exactly one of the two.

  • rev: pins. When the server resolves your packs (at template version resolution, or when a change is created from a git remote), every pack is frozen to an exact commit sha and recorded — a pinned version reproduces forever, and bumping a pack is a deliberate act.

  • Locally, git-sourced packs come from the pack cache. A local run never contacts a remote: run opschain packs fetch once in the repository and each remote:/url: declaration is resolved to an exact commit under <opschain home>/packs/<name>/<sha> (locally, remote: names one of the repository's own git remotes); every later local run mounts it offline, and fetching again is how a branch rev: moves. A pack manifest the tree carries is verified like a signed bundle, and one that does not verify is never cached (opschain docs cli/packs-fetch).

  • installed: mounts an uploaded bundle. A signed pack bundle installed on the platform (opschain admin packs install, or the Packs panel — the air-gapped distribution lane) mounts with installed: <pack name> instead of a git source:

    packs:
    - name: aws
    installed: opschain-pack-aws
    # version: 1.2.3 # optional: pin an exact installed version

    Add version: 1.2.3 to pin an exact installed version; omit it and resolution seals the pack's pinned version (if an administrator set one) or the highest installed version at template-version resolution time (opschain docs cli/admin-packs). To roll a newly installed version out across the estate, opschain templates re-resolve --pack <name> cuts one new template version per template that resolves the pack through an unversioned declaration, moving only the pack pin — explicit version: pins and already-sealed versions never move (opschain docs cli/templates-re-resolve). An unversioned declaration seals the estate default — the admin pin, else the highest installed version — unless you choose a version for one template version yourself: opschain templates version upsert <project>/<code> --version <v> --pack <name>=<version> (or "Select version" on the version page's Sealed packs panel) sets an explicit per-version pin that a later Fetch revision keeps, while every other pin re-resolves as usual; --pack <name>= clears it back to the default. A template version pins its Git SHA, and its pack versions the same way. Re-resolve's --version writes the same explicit pin on every version it cuts. A locked version takes no pin change — unlock it first.

  • bundle: mounts a signed bundle file in --local runs. The same signed .bundle artefact the installed store accepts can be mounted straight from disk for a local run — bundle: dist/mypack_v1.2.3.bundle (relative to the repository root, or absolute). The file is verified exactly as a server upload is — manifest signature, every per-file digest, and any entitlement its signed manifest requires — before anything runs, and ${{ pack.dir }} resolves into the verified content. There is no store and no pin: the file on disk is the version. A server never reads local disk, so on a server the same entry is a named refusal pointing at installed:.

  • References are always fully qualified. You call uses: script:execute; your own bare uses: references never accidentally resolve into a pack, so adding a pack can never change what existing actions do.

2. Call a pack action: script:execute​

The common pack's script namespace runs a script committed in your repository — no wrapper authoring at all. With the pack declared as above, add scripts/hello.sh and an action that calls it:

#!/bin/sh
# scripts/hello.sh
echo "hello from the script, $1"
# .opschain/actions/run_script.yaml
action: run_script
steps:
- name: say_hello
uses: script:execute
with: { path: scripts/hello.sh, args: "acme" }
- name: report
run: echo "script said ${{ steps.say_hello.outputs.stdout }}"

Plan it: the pack action's own step appears under yours, exactly like a composed action from lesson 2.2:

opschain runs create change run_script --env dev --plan --local
· run_script
├─ · say_hello
│ └─ · execute
└─ · report

Run it. The bundled pack needs no server and no network:

opschain runs create change run_script --env dev --local
2026-11-07T00:26:25Z run_script/say_hello/execute queued -> running
[run_script/say_hello/execute] ... script:execute running [ scripts/hello.sh ] via [ /bin/sh ] with 1 argument(s)
[run_script/say_hello/execute] hello from the script, acme
2026-11-07T00:26:26Z run_script/say_hello/execute running -> success
2026-11-07T00:26:26Z run_script/report queued -> running
[run_script/report] script said ... hello from the script, acme
2026-11-07T00:26:26Z run_script/report running -> success
2026-11-07T00:26:26Z run_script running -> success

run 01a1b366-4281-7358-960c-0f1dba0b7717 success (1.1s)

Pack actions declare typed inputs the same way lesson 2.2's composed actions do — path: here is required, so forgetting it fails at plan time with the missing input named, before anything runs. They publish outputs too: script:execute captures the script's stdout and exit_status for later steps to consume — no log-scraping.

On a server the same action runs as a change on an asset (lesson 3.1), and the step's log carries the pack provenance — which pack, from which remote, at which pinned sha — in the [system] preamble, with the script's own output after it.

Inside a pack's actions, ${{ pack.dir }} resolves to the pack's own checkout in the step workspace — that is how a pack ships helper scripts without your repository carrying them. Reference every pack-shipped file through it (run: '"${{ pack.dir }}/scripts/backup.sh"'): a bare relative path resolves against the estate workspace, not the pack. During the in-repo development loop (lesson 2.5) the same reference resolves to the repository checkout — the repo is the pack root there — so the one spelling works before and after the split-out.

3. The pack store, from the operator's seat​

installed: entries resolve against the platform's pack store — signed bundles uploaded as whole versioned units, the distribution lane for commercially licensed packs and for air-gapped estates. The verbs are administrative (opschain docs cli/admin-packs covers standing and flags); here is the loop end to end.

See what is installed:

opschain admin packs list
NAME VERSION REQUIRES MIN_PLATFORM PINNED REFERENCED BY FILES KEY
opschain-pack-infrastructure 1.2.3 packs:infrastructure 5.4.0 yes 0 tv / 0 runs 24 2026-a
opschain-pack-infrastructure 1.2.2 packs:infrastructure - no 2 tv / 3 runs 23 2026-a

Versions co-exist deliberately — different template versions can pin different pack versions and run side by side, and keeping prior versions is what makes rollback a re-pin. REFERENCED BY tells you, before you act, what holds each version: the template versions that sealed it (a re-resolve moves them onto another version) and the runs that executed from it (retention releases those) — a delete refuses while either count is non-zero. -o json lists the sealing template versions by project, template and version so you can find them; the template version's own page shows the same relation from its side (sealed_packs).

Install a signed bundle:

opschain admin packs install -f opschain-pack-infrastructure_v1.2.3.bundle
Installed opschain-pack-infrastructure version 1.2.3 (24 files, signed by 2026-test)

The server verifies the bundle offline against its embedded keyring before a byte is stored — manifest signature, every per-file digest — and each failure refuses by name (bad signature, digest mismatch, unknown signing key). Versions are immutable: re-uploading the same version with identical content is idempotent, with different content it refuses — a fix is always a new version.

Pin the version unversioned declarations get:

opschain admin packs pin opschain-pack-infrastructure 1.2.2
Pinned opschain-pack-infrastructure to version 1.2.2

Pinning steers future resolutions only — template versions that already resolved keep the exact version they sealed — which is exactly what makes it the rollback lever: pin the prior version, then re-resolve (opschain templates re-resolve --pack <name>). --clear restores the default, the highest installed version. To move just one template version, skip the estate pin and set its own: opschain templates version upsert demo/web_stack --version 1.1.0 --pack opschain-pack-infrastructure=1.2.2 — the version's sealed_packs then shows the pack as decided_by: explicit, and fetching its revision keeps the choice.

When the licence lacks the pack's entitlement. The REQUIRES column is the pack's own declaration: a commercially licensed pack's signed manifest names the packs:<name> entitlement it needs. Installing such a bundle on a licence without the entitlement is refused naming it:

this surface requires the "packs:infrastructure" entitlement - the basic licence does not include it; apply a licence that grants it (opschain admin licence apply)

And if the entitlement lapses after install, the stored pack stays put but creating a change that uses it refuses with the named error:

action pack "<pack>" (mounted as "<mount>") requires the "<feature>" entitlement — the <tier> licence does not include it; apply the licence pack that grants it (opschain admin licence apply / PUT /api/licence)

Applying the licence pack lights everything back up — nothing is reinstalled. Packs you write yourself carry no signed manifest and are never gated.

Read more: opschain docs pack-store (the store's lifecycle in full), opschain docs entitlements (tiers, grants and every named refusal), opschain docs packs-common (the bundled pack's action catalog).

Check yourself​

  • What does .opschain/packs.yaml pin, and what in a step's output tells you which pack action ran and how?

What you learned​

  • .opschain/packs.yaml mounts a pack under a name:, and the common pack resolves from the copy bundled with your binary when you name no source.
  • You call a pack action fully qualified (uses: script:execute), bind its typed inputs with with:, and read its outputs like any composed action.
  • The bundled pack runs in --local with no server and no network; rev:, installed: and bundle: are the other three sources — and a git-sourced pack runs locally too, from the pack cache opschain packs fetch fills.
  • The pack store holds signed, immutable, co-existing versions; a pin steers future resolutions and is the rollback lever.

Next: lesson 2.5 — Building a pack (opschain learn 2.5), or back to your track (opschain learn).