Skip to content

feat(model)!: validate SimpleAction steps with authored error paths - #419

Open
mwiebe wants to merge 1 commit into
OpenJobDescription:mainfrom
mwiebe:fix/simple-action-desugar-validation
Open

mwiebe wants to merge 1 commit into
OpenJobDescription:mainfrom
mwiebe:fix/simple-action-desugar-validation

Conversation

@mwiebe

@mwiebe mwiebe commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

What was the problem/requirement? (What/Why)

Open Job Description templates can define a step's work in two ways. The full form is a script block with a command, arguments, and embedded files. The short form (a "SimpleAction", from the FEATURE_BUNDLE_1 extension) is a bash:, python:, cmd:, powershell:, or node: block containing just the script text and optional arguments. Internally the short form is expanded ("desugared") into the full form before a job runs.

The two forms were not validated the same way:

  • openjd check validated only the full form. A short-form step was barely checked: undefined variables, a timeout of 0, an empty script, or a script body over an opted-in size cap all passed check.
  • Job creation did validate the short form, but on the expanded version, so error paths named nodes that do not exist in the author's template, e.g. steps[0] -> script -> embeddedFiles[0] -> data for a step that has no script.
  • A malformed {{ ... }} in a short-form script was only caught at job creation, with no field path at all.

What was the solution? (How)

Template validation now expands each short-form step and runs it through the same validation code as a full-form step, so both forms are held to the same rules by construction. Every error produced this way is then mapped back to the field the author actually wrote: embeddedFiles[0] -> data becomes bash -> script, argument indexes have the generated interpreter arguments subtracted so args[1] means the author's second argument, and anything synthesized (the generated command or file name) points at the bash: field itself. Job creation uses the same mapping.

Two smaller changes came with this. The short form's script field is now parsed as a format string when the template is loaded (the spec types it that way, and the Python reference does the same), so brace errors are reported at load time. The generated embedded file name is now built from ASCII characters only, matching the Python reference; some non-ASCII step names previously caused a panic.

What is the impact of this change?

Templates with short-form steps get the same diagnostics as full-form steps, at paths that exist in their template. Some templates that previously passed check will now fail it (undefined references, out-of-range literal timeout/notifyPeriodInSeconds, empty script or args, a let without the EXPR extension). Each of these was already invalid at job creation or run time.

How was this change tested?

  • cargo test --workspace: all pass (two openjd-sessions Windows cross-user tests fail on unmodified main in this environment and are unrelated).
  • 28 new tests in crates/openjd-model/tests/integration/test_simple_action_validation.rs assert the full path and message at both template validation and job creation, and assert that no diagnostic for a short-form step contains a desugared path.
  • 7 unit tests in step.rs cover the path-mapping table and step-name sanitization.
  • OpenJD conformance suite: 1139 passed, 0 failed.
  • cargo clippy -D warnings, cargo fmt --check, cargo doc -D warnings, and the copyright header check are clean.
  • An independent agent review was run; its findings (an encoding regression in comments, a missing copyright line, one case that still named a generated symbol, and the pre-existing panic) were all addressed.

Was this change documented?

Yes. Doc comments on all new public items. Specs updated: specs/model/validation.md (new "Step Scripts and SimpleActions" and "Reporting on desugared forms" sections with the path table), specs/model/job-creation.md, specs/model/template-types.md, and specs/model/public-api.md.

Is this a breaking change?

Yes, for users of the openjd-model crate API:

  • template::SimpleAction.script is now FormatString instead of String. Read the text with .raw().
  • StepTemplate::resolve_syntax_sugar() now returns Option<StepScript> instead of Result<Option<StepScript>, ModelError>. It can no longer fail.
  • New public items: template::SimpleActionKind, StepTemplate::simple_actions(), StepTemplate::simple_action().

No change to the template format, the CLI interface, or the job::* output types.

Does this change impact security?

No. Validation only; no files or processes are created or modified.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@mwiebe
mwiebe requested a review from a team as a code owner October 1, 2026 00:01
…authored error paths

Template validation only validated `step.script`; a SimpleAction step
(`bash:`/`python:`/`cmd:`/`powershell:`/`node:`, FEATURE_BUNDLE_1 §8)
was never checked beyond its `let` bindings, while `create_job` checked
the desugared form and reported paths like
`steps[0] -> script -> embeddedFiles[0] -> data` — a node the author
never wrote. So a 500-char `bash:` body under `max_resolved_data_len:
100` passed `check` and failed at submission, and undefined `Param.*`
references, `timeout: 0`, `notifyPeriodInSeconds: 700`, an empty
`script`, and a `let` without EXPR all passed `check`.

Passes 6 and 8 now desugar each SimpleAction field
(`SimpleActionKind::desugar`) and run it through the same helpers an
authored `script` gets (`validate_step_script`,
`validate_step_script_format_strings`), so the stages agree by
construction. Every diagnostic — at decode and at `create_job` — is
re-rooted onto the field the author wrote through one remap table
(`SimpleActionKind::remap_desugared_path`, applied by the crate-private
`ValidationErrors::extend_remapped`): `embeddedFiles[0] -> data` maps
to `<kind> -> script`, `args[k]` to `args[k - offset]` with the
interpreter prefix and generated file reference subtracted,
`timeout`/`cancelation`/`let[j]` pass through, and anything synthesized
collapses to the `<kind>` field itself. Sugar is skipped when the step
also has `script` (pass 7 reports the conflict; validating against a
symtab seeded from the authored script would name the generated file).

`SimpleAction.script` is now a `FormatString` — the spec types it
`<DataString>` `@fmtstring[host]`, and the Python reference agrees — so
malformed brace syntax fails at parse time instead of inside
`create_job` with no path, and `resolve_syntax_sugar` is infallible.
The step-name sanitizer now keeps only `[A-Za-z0-9]`, matching the
Python reference; non-ASCII alphanumerics (`²x`, `a½`) used to panic at
the generated `{{Task.File.<name>}}` reference.

The comprehension-variable check now also covers embedded-file `data`
for authored scripts, since the sugar's `script` desugars to it.

Pinned by 28 tests in `test_simple_action_validation.rs` asserting full
path + message at both stages, including that no diagnostic for a sugar
step names the desugared form. Conformance 1139/1139.

BREAKING CHANGE: `template::SimpleAction.script` is `FormatString`
instead of `String`; read it with `.raw()`.
`StepTemplate::resolve_syntax_sugar` returns `Option<StepScript>`
instead of `Result<Option<StepScript>, ModelError>`. New public
`template::SimpleActionKind` and `StepTemplate::simple_actions` /
`simple_action`.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe force-pushed the fix/simple-action-desugar-validation branch from 5f0a91d to 2147dc9 Compare October 1, 2026 00:05
@mwiebe
mwiebe enabled auto-merge (squash) October 1, 2026 02:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant