6.5 KiB
title, description, weight
| title | description | weight |
|---|---|---|
| Actions and the overlay | The action contract and how node-local proprietaries shadow commons actions. | 13 |
Every unit of automation in apex is an action: a single Python module that the engine dispatches by name. The overlay lets a node's proprietaries add, override, or disable commons actions without forking them. This page defines the action contract, the declarative argument descriptors, and the resolution rules.
The action contract
An action is one .py file under an actions/ tree. The file path is the action name: actions/<group>/<action>.py is invoked as apex <group>/<action> [args...]. There is no registry to update — dropping a file into the tree publishes the action.
The module must expose two things:
METADATA = Meta(...)→ the summary and argument descriptors that drive both the help text and parsing. The descriptor system succeeds theaction_metadata()heredoc of the bash "root" commons framework, apex's predecessor.run(ctx, args)→ the entrypoint.ctxis the engine-built execution context;argsis the parsedargparsenamespace.
from engine.descriptor import Meta, Arg, Flag
METADATA = Meta(
summary="One-line summary shown in the action listing",
args=[
Arg("target", help="A required positional argument"),
Flag("--dry-run", help="A boolean flag"),
],
)
def run(ctx, args):
...
return 0
The return value of run becomes the process exit code. A non-integer return (including None) is treated as success (0). An unhandled exception is logged with the context prefix and exits 1.
Note
METADATAis technically optional — a module without it is dispatched with an emptyMetaand accepts no arguments — but every real action should declare it, since the listing and--helpoutput are generated from it.
Declarative arguments
Meta.args is a list of descriptors. Each maps to exactly one argparse construct:
| Descriptor | Maps to | Behavior |
|---|---|---|
Arg("name") |
positional argument | Required by default; required=False makes it an optional positional (nargs="?", default None). |
Opt("--name", default=...) |
value option | An optional flag that takes a value, with a declared default. |
Flag("--name") |
boolean flag | store_true; present means True. |
Rest("name") |
remainder | Greedy passthrough of all remaining argv (argparse.REMAINDER). |
The engine builds the parser as apex <group>/<action> and parses the remaining argv before calling run. --help is handled at parse time. A usage error — unknown flag, missing positional — prints usage and exits 64 (EX_USAGE) instead of argparse's default 2.
Listing without importing
apex with no arguments (or --help) lists every available action. Listing never imports action modules: the engine statically scans each file with the ast module and extracts only the METADATA summary and any DISABLED constant. Action code therefore cannot run — or crash — at list time; a file that fails to parse is listed as (unparseable) rather than breaking the listing.
Each entry is tagged with its overlay source:
- no tag → a commons action,
(local)→ exists only in proprietaries,(local override)→ a proprietaries action shadowing a commons one,[DISABLED: reason]→ the action declaresDISABLED.
The overlay
Action names are resolved across two trees, proprietaries first:
{{< filetree/container >}} {{< filetree/folder name="node repository" state="open" >}} {{< filetree/file name="node.env" >}} {{< filetree/folder name="commons" state="open" >}} {{< filetree/folder name="engine" state="closed" >}}{{< /filetree/folder >}} {{< filetree/folder name="actions" state="open" >}} {{< filetree/file name="/.py" >}} {{< /filetree/folder >}} {{< /filetree/folder >}} {{< filetree/folder name="proprietaries" state="open" >}} {{< filetree/folder name="actions" state="open" >}} {{< filetree/file name="/.py" >}} {{< /filetree/folder >}} {{< /filetree/folder >}} {{< /filetree/folder >}} {{< /filetree/container >}}
The engine detects this layout by its own location: when it runs from a commons/ directory whose parent contains node.env, the parent is the node repository root and proprietaries/actions/ is the overlay tree. A standalone commons checkout is its own root and has no overlay layer. Inside a node repository, node identity is resolved from the host FQDN before dispatch.
Resolution for a given name checks proprietaries/actions/<name>.py first and commons/actions/<name>.py second; exactly one module is loaded by path. An unknown name is an error (exit 1). The overlay supports four moves:
- Add → a file that exists only in proprietaries becomes a node-local action, listed as
(local). - Override → a proprietaries file at the same relative path shadows the commons implementation entirely; the commons path is recorded on the resolution as
shadowed. - Wrap → an overriding module can delegate to the implementation it shadows via
ctx.commons.run(args), adding node-local behavior before or after the commons logic instead of replacing it. - Disable → a module that assigns
DISABLED = "reason"is refused at dispatch: the engine logs the reason and exits1. The listing shows[DISABLED: reason]. A bareDISABLED = Truealso disables, with the generic reasondisabled; an empty string does not.
Exit codes
Actions and the engine follow a fixed exit-code convention:
| Code | Meaning | Typical cause |
|---|---|---|
0 |
success | run returned 0 or a non-integer. |
1 |
general error | Unknown action, disabled action, unhandled exception, non-integer SystemExit. |
64 |
usage error | Arguments rejected by the parser (EX_USAGE). |
65 |
data error | Malformed input data (EX_DATAERR), raised by engine libraries. |
66 |
missing input | A required input is absent (EX_NOINPUT), raised by engine libraries. |
See also: