Files
ermnvldmr.com/services/docs/content/en/apex/guides/action-development.md
T
deytenit b3638f4d63 fix: cyrillic fonts, dev certs, page head, and ru-text typography (#67)
🤖 PR generated for branch `fix/cyrillic-fonts-dev-certs-and-page-head`.

### Changes Summary
- **Fonts**: Add Cyrillic glyph subsets for Lato, EB Garamond, and Fira
Code.
- **Dev**: Support remote Root CA and encrypted dev CA key in
`.env.dev`.
- **UI & Kits**: Add size variant to `Breadcrumbs` component and enlarge
`PageHead` breadcrumbs with tuned collapse scroll threshold.
- **ESLint**: Add `no-hanging-particles` i18n & markdown typography rule
with auto-fixer.
- **Content & i18n**: Apply `ru-text` editorial and typography polish
(em dashes, number ranges, «ё» restoration, non-breaking spaces) across
`services/www` and `services/docs`.
2026-10-25 15:21:09 +03:00

4.1 KiB

title, description, weight
title description weight
Develop actions Write, override, and test engine actions in commons or proprietaries. 22

Context: all automation ships as actions — one Python module each, dispatched by path. Shared behavior belongs in 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

"""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 is 64 usage, 65 data error, 66 missing input, 1 general.

Conventions for unattended actions

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.
    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

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:

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

The commons test suite is stdlib unittest:

./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: