Files
ermnvldmr.com/services/docs/content/en/apex/guides/action-development.md
T

91 lines
4.0 KiB
Markdown

---
title: Develop actions
description: Write, override, and test engine actions in commons or proprietaries.
weight: 22
---
**Context:** all automation ships as [actions](/apex/glossary#action) — one Python module each, dispatched by path. Shared behavior belongs in [commons](/apex/glossary#commons) `actions/`; anything node-specific belongs in that node's `proprietaries/actions/`. Both use the identical contract, and the launcher puts both trees on `sys.path`, so a proprietary action imports `engine.*` exactly like a commons one.
## A minimal action {#minimal}
```python {filename="proprietaries/actions/utils/example.py"}
"""One-line module docstring describing the action."""
import os
from engine.descriptor import Meta, Arg, Flag
METADATA = Meta(
summary="Copy a rendered config into place.",
args=[Arg("target_dir", "Destination directory"),
Flag("--dry-run", "Preview only.")],
)
def run(ctx, args):
if not os.path.isdir(args.target_dir):
ctx.log.error(f"No such directory: {args.target_dir}")
raise SystemExit(66)
content = ctx.tpl.render("example/config", ctx.vars())
ctx.host.write_file(os.path.join(args.target_dir, "config"), content,
dry_run=args.dry_run, sudo=True)
ctx.log.success("Config deployed.")
```
The file's path is its name: the module above dispatches as `apex utils/example`. Return `0`/`None` for success, an integer for a specific exit code, or raise `SystemExit(code)`; the [exit-code convention](/apex/concepts/actions-and-overlay#exit-codes) is `64` usage, `65` data error, `66` missing input, `1` general.
## Conventions for unattended actions {#unattended}
Actions that run from cron follow two rules so failures page instead of vanishing:
- **Wrap the body** in `try/except Exception` and send `ctx.notify.error(...)` before re-raising as `SystemExit(1)` — the Python equivalent of a shell ERR trap.
- **Gate the exit code on the final notification**: `ctx.notify.*` returns `False` on delivery failure; a completed run whose status message never arrived should still exit `1`.
```python
try:
...work...
log.success("Done.")
if not ctx.notify.success(TITLE, url, "Done.", node):
raise SystemExit(1)
except SystemExit:
raise
except Exception as e:
log.error(f"{type(e).__name__}: {e}")
ctx.notify.error(TITLE, url, f"Critical error: {e}", node)
raise SystemExit(1)
```
## Override, wrap, disable {#override}
A proprietary file at the same relative path as a commons action shadows it completely. Inside the shadowing module, `ctx.commons.run(args)` invokes the shadowed implementation — the wrap pattern:
```python {filename="proprietaries/actions/sync/packages.py"}
from engine.descriptor import Meta, Arg
METADATA = Meta(summary="sync/packages with a node-local pre-step.",
args=[Arg("telegram_bot_url", "Telegram Bot URL")])
def run(ctx, args):
ctx.log.info("Node-local preparation...")
return ctx.commons.run(args) # delegate to the commons implementation
```
To retire an action on one node, assign `DISABLED = "reason"` at module top level: dispatch refuses it (exit `1`) and the listing shows `[DISABLED: reason]`.
## Testing {#testing}
The commons test suite is stdlib `unittest`:
```bash
./tests/run.sh # python3 -m unittest discover -s tests -p 'test_*.py' -v
```
Engine changes must keep the suite green. A cheap smoke gate for any action — commons or proprietary — is its parser: `apex <name> --help` must exit `0`, which proves the module parses, its `METADATA` is well-formed, and discovery sees it.
The commons pre-commit hook (wired by `init.sh` via `core.hooksPath`) runs `apex utils/lint-docker-compose --hook` against staged compose files, so composition changes are linted at commit time.
---
**See also:**
- [Concept: Actions and the overlay](/apex/concepts/actions-and-overlay)
- [Concept: The execution context](/apex/concepts/execution-context)
- [Reference: Engine API](/apex/reference/engine-api)