Skip to content

feat(recipe): run a recipe from a file, parsed strictly - #213

Merged
kuny0707 merged 1 commit into
tronprotocol:developfrom
barbatos2011:feat/recipe-run-from-file-v2
Aug 6, 2026
Merged

kuny0707 merged 1 commit into
tronprotocol:developfrom
barbatos2011:feat/recipe-run-from-file-v2

Conversation

@barbatos2011

Copy link
Copy Markdown

recipe run and recipe show could only reach the five recipes embedded in the binary, so the workflows this is meant to replace could not be expressed at all. Both now take --file <path>, and a new recipe validate --file parses and checks one without running a step.

$ trond recipe validate --file ./my.yaml
my-own-recipe: ok — 1 steps, 1 params

$ trond --require-private recipe run --file ./my.yaml --param node=n1 --dry-run
  [check] would run: trond status --output json --state-dir SD --require-private n1

$ trond recipe run --file ./my.yaml --param node=n1 --dry-run -o json | jq -r .source
./my.yaml

Exactly one source, never both: a command handed a name and a --file refuses rather than silently preferring one, since the caller would then be running something other than what they named. The result records where the recipe came from (source: "builtin:<name>" or the path) — without it a run from disk is indistinguishable in its output from a run of the built-in with the same name.

Why the file loader is strict where the embedded one is not

The two have different failure modes. An embedded recipe is reviewed at build time and its tests run in CI. A file handed to --file is typed by someone who wants it to run now, against real nodes.

  • Unknown fields are errors. on_failre: continue parses clean under lenient YAML and silently means "abort" — the recipe does the opposite of what it says, at the moment a step has already failed.
  • Multi-document files are refused. yaml.Unmarshal returns document one and says nothing, so a file holding two recipes runs half of what it contains.
  • Structure is validated up front, reporting every problem at once — missing ids, missing commands, an on_failure outside abort/continue/rollback, duplicate step ids, duplicate params. A recipe is a sequence of real operations: the expensive place to discover a typo is step four, after steps one to three have already changed something. Reporting one problem per run means one run per typo.

A test asserts every embedded recipe passes the new validation, so a rule that would reject something we ship fails here rather than in a user's hands.

Schema

recipe validate gets a real schema rather than an entry in TestSchemaCoverage's accepted-gaps list, whose own comment says the goal is to drive that list to zero.

SchemaVersion 1.12.5 → 1.13.0 — two additive changes (the source property on recipe-run, the new recipe-validate file), baseline re-snapshotted. Both copies under schemas/output/ and internal/schema/files/ are byte-identical, as the drift test requires.

Testing

  • make test -race — 25 packages, 0 failures
  • make lint — no findings
  • Nine new tests in internal/recipe/load_test.go covering strict parsing, multi-document rejection, empty input, the all-problems-at-once validator, and the embedded-recipe compatibility check
  • Exercised end to end through the CLI: validate, run --file with the gate forwarded, source in the JSON result, the both-sources refusal, and the error text for a misspelled field

Deliberately not here

No --expect-sha256 provenance pin. A recipe's steps are trond subcommands, each individually gated (#211) and audited, so --file does not widen what trond can do — it changes who typed the sequence. A checksum pin protects against a file changing between review and run, which is a real concern but a separate one, and it belongs with kind: host, where the capability ceiling actually moves.

`recipe run` and `recipe show` could only reach the five recipes embedded
in the binary, so the workflows this is meant to replace could not be
expressed at all. Both now take `--file <path>`, and a new
`recipe validate --file` parses and checks one without running a step.

Exactly one source, never both: a command handed a name AND a --file
refuses rather than silently preferring one, since the caller would then
be running something other than what they named. The result records
where the recipe came from (`source`: "builtin:<name>" or the path) —
without it a run from disk is indistinguishable in its output from a run
of the built-in with the same name.

The file loader is strict where the embedded loader is lenient, because
the two have different failure modes. An embedded recipe is reviewed at
build time and its tests run in CI. A file handed to --file is typed by
someone who wants it to run now, against real nodes:

- **Unknown fields are errors.** `on_failre: continue` parses clean under
  lenient YAML and silently means "abort" — the recipe does the opposite
  of what it says, at the moment a step has already failed.
- **Multi-document files are refused.** yaml.Unmarshal returns document
  one and says nothing, so a file holding two recipes runs half of what
  it contains.
- **Structure is validated up front, reporting every problem at once.**
  Missing ids, missing commands, an on_failure outside
  abort/continue/rollback, duplicate step ids, duplicate params. A recipe
  is a sequence of real operations: the expensive place to discover a
  typo is step four, after steps one to three have already changed
  something. Reporting one problem per run means one run per typo.

A test asserts every embedded recipe passes the new validation, so a rule
that would reject something we ship fails here rather than in a user's
hands.

`recipe validate` gets a real schema rather than an entry in
TestSchemaCoverage's accepted-gaps list, whose comment says the goal is
to drive that list to zero. SchemaVersion 1.12.5 -> 1.13.0: two additive
changes (the `source` property, the new file), baseline re-snapshotted.

Deliberately not here: no `--expect-sha256` provenance pin. A recipe's
steps are trond subcommands, each individually gated and audited, so
--file does not widen what trond can do — it changes who typed the
sequence. A checksum pin protects against a file changing between review
and run, which is a real concern but a separate one, and it should land
with `kind: host` where the capability ceiling actually moves.
@kuny0707
kuny0707 merged commit 8c224ca into tronprotocol:develop Aug 6, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants