Skip to main content
Version: v5.6.1

2.7 Lab: author a pack action and run it off the tree

Included from Basic tier

After this you can: apply lessons 2.1–2.6 without new verbs: author a pack action of your own, plan it, run it off the working tree, and rehearse the commit.

Before you start — tick each one:

  • Lesson 2.5 — Building a pack (opschain learn 2.5)
  • Lesson 2.6 — The local development loop (opschain learn 2.6)

In the GUI: none — the lab is a working-tree exercise.

This lab introduces nothing new. You will author one pack action with an input and a summary, plan it, run it off your working tree, rehearse the commit, and push — every step with the output it should show. Work in the repository lesson 2.1 scaffolded (any git repository with at least one commit and an .opschain/ directory will do). Each step ends with the lesson to re-read if your output did not match.

1. Author the pack action​

Create .opschain/actions/mypack/banner.yaml:

action: banner
summary: Print a banner naming the release
inputs:
release: { required: true }
steps:
- name: print
run: echo "=== release ${{ inputs.release }} ==="

The directory mypack/ makes the action mypack:banner; the inputs: block declares the one value a caller must supply. Check the catalog picked it up:

opschain actions list --local
ACTION SOURCE INPUTS APPROVALS STATUS SUMMARY
hello repo no no Example action scaffolded by opschain init
mypack:banner repo no no Print a banner naming the release

If a step did not show that, re-read lesson 2.5 (the namespace directory, summary:) or lesson 2.1 (actions list, a file listed as invalid: with the reason).

2. Plan it​

opschain runs create change mypack:banner --env dev --plan --local -i release=1.2.0
· mypack:banner
└─ · print

Then plan it again without the input, to see the refusal the plan owes you:

opschain runs create change mypack:banner --env dev --plan --local
Error: action mypack:banner input release is required and was not supplied

If a step did not show that, re-read lesson 2.2 (inputs and how they are validated at compile time).

3. Run it off the working tree​

Nothing is committed yet; a local run snapshots the working copy anyway:

opschain runs create change mypack:banner --env dev --local -i release=1.2.0
2026-11-07T00:25:50Z mypack:banner pending -> queued
2026-11-07T00:25:50Z mypack:banner queued -> running
2026-11-07T00:25:50Z mypack:banner/print pending -> queued
2026-11-07T00:25:50Z mypack:banner/print queued -> running
[mypack:banner/print] === release 1.2.0 ===
2026-11-07T00:25:50Z mypack:banner/print running -> success
2026-11-07T00:25:50Z mypack:banner running -> success

run 01a1b365-b942-7bb1-8a7c-6761728c5d89 success (0.1s)

The exit code is 0. Read the log back by run id:

opschain logs 01a1b365-b942-7bb1-8a7c-6761728c5d89 --local
[mypack:banner/print] === release 1.2.0 ===

If a step did not show that, re-read lesson 2.1 (the local run and its exit codes) or lesson 2.6 (runs list|show and logs with --local).

4. Rehearse the commit with --ref​

First prove the rehearsal catches an uncommitted file — run the ref before committing:

opschain runs create change mypack:banner --env dev --local --ref main -i release=1.2.0
Error: action "mypack:banner" has no declarative action file, so typed inputs cannot be supplied — inputs bind to an action file's inputs: declarations (see: opschain docs orchestration-logic)

The commit on main does not carry the file, so the ref run cannot find the action — exactly the gap the rehearsal exists to expose. Now commit and run the ref again:

git add .opschain/actions/mypack/banner.yaml
git commit -m "mypack: banner action"
opschain runs create change mypack:banner --env dev --local --ref main -i release=1.2.0
2026-11-07T00:25:51Z mypack:banner/print queued -> running
[mypack:banner/print] === release 1.2.0 ===
2026-11-07T00:25:51Z mypack:banner running -> success

run 01a1b365-bc34-731b-992e-469b83b00afa success (0.1s)
opschain runs show 01a1b365-bc34-731b-992e-469b83b00afa --local
Run: 01a1b365-bc34-731b-992e-469b83b00afa
Kind: change
Action: mypack:banner
Target: myrepo/dev
Status: success
Commit: eaa9ecfe14a700eb12dd773fb5e540e1281a90f5
...
✔ mypack:banner — success (0.1s)
└─ ✔ print — success (0.0s)

The Commit: line is the revision you just committed — the run executed the commit, not your tree.

If a step did not show that, re-read lesson 2.6 (rehearsing with --ref).

5. Push and promote​

Push the commit:

git push origin main

What you pushed is now all a server can see of your work. Promotion happens on the server lane, in the next module: a change on an environment of yours runs this action with the server keeping the record (lesson 3.1), and a template version pins the exact commit you just rehearsed so pinned assets run precisely that (lesson 3.4). When you get there, the create line you are looking for is:

created change 01a1… (mypack:banner on acme/dev) — pending

If a step did not show that, re-read lesson 2.6 (why a server never reads your local disk) — and then opschain learn 3.1.

Check yourself​

  • Does your pack action plan clean, refuse without its input, run green off the tree, and run green again with --ref pointing at the committed revision — with runs show naming that commit?

What you learned​

  • A pack action is a namespaced action file with a summary: and declared inputs:, listed by actions list --local.
  • --plan catches a missing input before anything runs; --local runs your working copy; --ref runs only what is committed.
  • A ref run against a commit that lacks the file is refused by name — the rehearsal exists to catch exactly that.
  • Pushing the rehearsed commit is the hand-off to the server lane, where lessons 3.1 and 3.4 promote it.

Next: this closes the module Author work — back to your track (opschain learn).