Skip to content

feat!: Bump openjd-* Rust crates to the 0.10.0 release - #373

Merged
leongdl merged 2 commits into
OpenJobDescription:mainlinefrom
leongdl:bump/openjd-rs-0.10.0
Sep 28, 2026
Merged

leongdl merged 2 commits into
OpenJobDescription:mainlinefrom
leongdl:bump/openjd-rs-0.10.0

Conversation

@leongdl

@leongdl leongdl commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes: n/a (dependency bump for OpenJobDescription/openjd-rs#408)

BREAKING CHANGE: openjd.model._v1.template.TemplateAmountRequirement.name and
TemplateAttributeRequirement.name are openjd.expr.FormatString instead of str.
Read the template text with .raw(); construct with FormatString(...). The job-side
AmountRequirement.name / AttributeRequirement.name are unchanged and still str.
No known consumer is affected — openjd-sessions-for-python and the Deadline Cloud
data plane both read the job-side type, and a code search across the
OpenJobDescription and aws-deadline orgs finds no reader of the template-side
capability name outside this repository.

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

openjd-rs released on 2026-09-28: openjd-expr 0.10.0, openjd-model 0.10.0, openjd-sessions 0.7.1. This package pinned 0.9.0 / 0.9.0 / 0.7.0. openjd-sessions 0.7.1 is a transitive re-pin with no source change.

The release is two features, both breaking:

PR Change
openjd-rs#409 template::AmountRequirement::name and template::AttributeRequirement::name become FormatString instead of String, so a capability name may contain expressions (openjd-specifications#189). The §3.3.1.1 / §3.3.2.1 constraints apply to the resolved name.
openjd-rs#407 create_job's context must cover the template's declared extensions. With mismatch impossible, the job-creation resolved-value checks report every evaluation error instead of silently skipping it.

Only #409 broke compilation, in four places: the two template-type constructors and the two name getters.

#407 is a squash of five commits, and its commit message documents three behaviour changes that neither the release changelog nor the PR body names. I found them by diffing the published crate sources tag to tag and reading the commit messages behind each changed file:

Id Change Named in the changelog?
407-d eval_boolop no longer suppresses budget errors in operands after an unresolved one — the same bypass eval_ifexp had no
407-e a list comprehension over a concrete iterable whose filter evaluates unresolved concludes unresolved[list[T]] instead of hard-erroring no
407-g template validation and create_job's re-checks evaluate under PathFormat::Posix, so validation outcomes no longer depend on the host OS no

407-e is the one that matters. It is a defect that predated the release, masked by the lenient error policy #407 deleted, and reachable only at job creation — the one stage where the iterable is concrete and the filter is not. A template like

args: ["{{ [f for f in Param.Files.split(',') if f != Task.Param.Skip] }}"]

passed openjd check (iterable unresolved, the tolerant path), ran cleanly on workers (everything bound), and was rejected at create_job.

What was the solution? (How)

Bump the three pins, adapt the four call sites, and pin every behaviour change that reaches this package's public API.

The Python-facing name becomes an openjd.expr.FormatString too, matching min / max / anyOf / allOf and Action.command — every other FormatString-typed field on the v1 template types, all of which already refused a bare str. Reading the template text needs .raw(); constructing one needs FormatString(...).

The first revision of this PR kept name a str and parsed on the way in, on the grounds that v0 models these as AmountCapabilityName / AttributeCapabilityName, both subclasses of v0's FormatString, which subclasses str. @mwiebe pointed out that this looks at the wrong neighbour: consistency with the sibling v1 fields matters more than consistency with v0. He is right, and the change is a net deletion — parse_capability_name and its doc comment go away because the FormatString arrives already parsed, and both constructors go back to infallible. Validation lands where it belongs: a malformed name is rejected by FormatString's own constructor rather than by a str-typed field parsing behind the caller's back.

The job-side AmountRequirement.name / AttributeRequirement.name stay str: job::AmountRequirement::name is still a String upstream, because it holds the resolved name.

create_job needed no code change. The binding derives its context from job_template.default_validation_context() when the caller passes none, which covers the template's extensions by construction. A caller-supplied mismatched context is exactly what #407 now rejects, and the binding surfaces it as ModelValidationError.

What is the impact of this change?

Behaviour changes, each measured through this package's API on both 0.9.0 and 0.10.0.

openjd-rs#409, through openjd.model._v1:

Case 0.9.0 0.10.0
decode_job_template, amounts[0].name = "{{Param.Attr}}" name '{{Param.Attr}}' does not match capability name pattern. accepted; .name is the raw text
same for attributes[0].name same rejection accepted
literal bogus.name rejected rejected, message unchanged
fully static {{ 'bogus.static' }} rejected on the raw text rejected on the resolved text; field path gains -> name
fully static, 101 characters unreachable exceeds 100 characters.
partly static, "amount.custom.<95 chars>{{Param.Attr}}" unreachable resolves to at least 109 characters, exceeding the maximum of 100.
static name duplicating a literal sibling unreachable duplicate amount name 'amount.custom.x'.
static standard attribute name with anyOf: [plan9] unreachable value 'plan9' is not valid for attr.worker.os.family.
static standard attribute name with a two-element allOf unreachable single-valued attribute cannot have more than 1 element.
{{Task.Param.F}} in a name rejected as a bad pattern Undefined variable: 'Task.Param.F' with caret — a name resolves at job creation, where task parameters are not bound
create_job, resolved amount.custom.x / attr.custom.x unreachable written onto the job
create_job, resolved not a name unreachable does not match capability name pattern.
create_job, resolved 101 characters unreachable exceeds 100 characters. (100 accepted)
create_job, resolved amount.worker.made_up unreachable uses reserved scope 'worker'.
create_job, resolved AMOUNT.CUSTOM.X vs literal amount.custom.x unreachable duplicate amount name 'AMOUNT.CUSTOM.X'.
create_job, resolved attr.worker.os.family with anyOf: [plan9] unreachable the resolved name selects the standard-capability value rules

openjd-rs#407:

Id Case 0.9.0 0.10.0
407-a create_job with a context stripping the template's EXPR job created create_job requires a context enabling every extension the template declares: missing EXPR.
407-a context derived from the template, or omitted, or a superset job created job created
407-b args[0] = "{{ 10 // Param.N }}", N=0 job created, error silently skipped Division by zero with caret at steps[0] -> script -> actions -> onRun -> args[0]
407-c 'A' * 100000 if Session.Flag else 'B', 5-operation budget unresolved[string] operation count (394) exceeded limit (5)
407-c same, 1024-byte budget unresolved[string] memory usage (100136 bytes) exceeded limit (1024 bytes)
407-c via create_job with CallerLimits(max_eval_operations=5) job created operation count (395) exceeded limit (5)
407-d Session.Flag and len('A' * 100000) > 0, 5-operation budget unresolved[bool] operation count (396) exceeded limit (5)
407-e [f for f in 'a,b,c'.split(',') if f != Task.Skip] List comprehension filter must be a boolean, got unresolved[bool] unresolved[list[string]]
407-e [10 // x for x in [0, 2] if x > Task.N] same error unresolved[list[int]] — the body is typed under an unresolved loop variable, so an element the run-time filter may exclude cannot raise

In every budget case a value error in the same position is still absorbed, because a run-time short-circuit may never reach it. A comprehension filter whose type can never be a boolean is still an error. Both are pinned as negative controls.

Public contract — this breaks the v1 Python API in one field. TemplateAmountRequirement.name and TemplateAttributeRequirement.name change from str to openjd.expr.FormatString, in both directions. A caller reading .name needs .raw(); a caller constructing one needs FormatString(...). The .pyi stub is updated to match.

Three pre-existing tests are the evidence that this is a behaviour change and not a refactor, and all three are updated: TestPickle::test_amount_requirement and test_attribute_requirement both passed name= a str, and TestStepTemplate::test_host_requirements compared .name to one.

Two other behaviour changes reach existing callers. A template with a format-string capability name now decodes where it used to be rejected, which is the point of #409. And a malformed name is now rejected — at FormatString construction rather than at the requirement's.

Cargo.lock moves only the three crates. THIRD-PARTY-LICENSES.txt was regenerated with scripts/check_third_party_licenses.sh --update and changed only the three version lines: the release pulled in no new transitive crates.

How was this change tested?

hatch run test: 6245 passed, 24 skipped, 3 xfailed, coverage 94.32%. The three xfails are the pre-existing openjd.expr known gaps; none flipped, so nothing in this release closed them. hatch run lint (ruff, black, mypy) clean. cargo fmt --all --check and cargo clippy -p openjd-python --all-targets -- -D warnings clean.

The openjd.expr expectations are copied from the upstream assertions in crates/openjd-expr/tests/integration/test_unresolved_eval.rs, not from its prose — all five comprehension cases agree.

New tests, each with a negative control:

  • test_parse.py: TestFormatStringCapabilityNames (decode acceptance, the pattern check, the expression-error path), TestFormatStringCapabilityNameChecksAtValidation (empty and over-length static names, let-bound static names, the partly-static lower bound, static-vs-literal uniqueness, single-reporting of a literal duplicate, the standard-attribute value and single-valued-allOf rules, a job-creation-unavailable symbol, and the deferral control)
  • test_create_job.py: TestFormatStringCapabilityNamesAtJobCreation (every rule parametrized over both amounts and attributes, plus the 100/101-character boundary and the resolved name selecting the standard-capability value rules), TestCreateJobContextExtensionContract (all four context shapes), TestValueDependentEvaluationErrorAtJobCreation, TestEvaluationBudgetsInsideUnresolvedConditionals, TestUnresolvedFilterComprehensionAtJobCreation, TestValidationIsIndependentOfTheHostPathFormat
  • test/openjd/expr/test_unresolved_eval.py: TestConcreteIterableUnresolvedFilter, TestBoolOpBudgetErrorsPropagate and TestIfExpBudgetErrorsPropagate (both parametrized over both arms — and/or, if-branch/else-branch — plus the composition of the two exemptions)
  • test_template_types.py: TestCapabilityNameIsAFormatString (the FormatString round trip, the refusal of a bare str, the malformed-name rejection, __repr__ stability, pickle)

Mutation check — four mutants, each rebuilt from source and each caught:

Mutant Result
the three version pins reverted to 0.9.0, with the pre-bump bindings 68 failed
both __repr__ sites format the FormatString instead of .raw() 2 failed
both name getters return a constant 12 failed
both constructors discard the supplied name 10 failed

Each mutant was restored byte-for-byte (checksum-verified) with the bytecode cache cleared between runs. The tests that survive the pin revert are the deliberate negative controls; a separate liveness mutant that flipped each control's expected value failed all of them.

Three independent auditors reviewed the tests (does each pin the behaviour, what does it fail to cover, craft and conventions). The gap auditor's first pass returned a fail with ten uncovered behaviours — the whole attribute half of #409 at job creation, the decode-time static-name checks, the or arm of 407-d and the else-branch of 407-c, and the partly-static lower bound. All were confirmed reachable by probe and are now covered; the tables above are the post-fix state.

What could NOT be verified

407-g, the POSIX path-format change, is Windows-only in effect. On a POSIX host the host format is POSIX, so there is nothing to observe on macOS or Linux. On 0.9.0 a PATH value flowing from a let binding into an argument drew Path format mismatch on Windows — 11 conformance failures upstream. TestValidationIsIndependentOfTheHostPathFormat pins the construction and is a no-op assertion locally; it earns its keep on the Windows CI lane only, and its docstring says so.

407-f, SymbolTable::set failures in check-symtab seeding now propagating as ModelError rather than degrading the check to a no-op, is unreachable from here by construction. Upstream's commit message states the seed keys are uppercase-rooted while let-binding names must start lowercase, so a collision requires an internal invariant to already be broken. No test added.

TestUnresolvedFilterComprehensionAtJobCreation does not discriminate 0.9.0 from 0.10.0. The template creates a job on both, for different reasons: 0.9.0's lenient policy silently skipped the error, 0.10.0 raises none. It is there because the two halves of the release have to hold together — #407's strict policy without its companion comprehension fix rejects that template — but it is not evidence that 407-e landed. The openjd.expr cases are. Its docstring says this.

Was this change documented?

Yes.

  • specs/python-model-interface.md — a table of which stage applies the capability-name constraints for a literal, fully static, partly static and parameter-dependent name; the symbol scope available to a name; the FormatString type of the template-side name against the str of the job-side one; the create_job context contract with its error message, replacing text that described passing a divergent profile as supported policy; the strict evaluation-error policy; and the OS-independence of validation outcomes. Also corrected ModelProfile.from_strings(SpecificationRevision.V2023_09, ...) to v2023_09 in the same example — V2023_09 is not an attribute, so that line raised AttributeError as written.
  • specs/python-expr-interface.md — the three unresolved-propagation rules that changed, stated as rules rather than as release notes.
  • src/openjd/_openjd_rs.pyi — the name type on both classes, with a docstring pointing at .raw(). Hand-edited; scripts/generate_stubs.sh does not run on macOS.
  • Doc comments on both name getters in rust-bindings/src/model/template_types.rs.

Is this a breaking change?

Yes, in one field. TemplateAmountRequirement.name and TemplateAttributeRequirement.name are openjd.expr.FormatString instead of str. A caller reading the template text uses .raw(); a caller constructing one wraps in FormatString(...). The job-side equivalents are unchanged.

Nothing else is removed or narrowed. See the impact section for the two non-breaking behaviour changes: a format-string capability name now decodes, and a malformed one is now rejected.

Does this change impact security?

Indirectly, in the direction of enforcement. Two ways a caller's lowered evaluation budget silently stopped applying are closed — inside a conditional whose test only a worker can resolve, and in an and / or operand after an unresolved one. Both are the idiomatic constructions for a worker-resolved value, so the bypass was reachable rather than theoretical. Separately, a value-dependent evaluation failure now fails submission instead of every session that runs the job.


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

openjd-rs released on 2026-09-28: openjd-expr 0.10.0, openjd-model 0.10.0,
openjd-sessions 0.7.1 (OpenJobDescription/openjd-rs#408). This package
pinned 0.9.0 / 0.9.0 / 0.7.0. openjd-sessions 0.7.1 is a transitive re-pin
with no source change.

Two breaking changes, both features:

- #409 makes template::AmountRequirement::name and
  template::AttributeRequirement::name a FormatString instead of a String,
  so a capability name may contain expressions
  (openjd-specifications#189). The §3.3.1.1 / §3.3.2.1 constraints now
  apply to the resolved name.
- #407 requires create_job's context to cover the template's declared
  extensions. With mismatch impossible, the job-creation resolved-value
  checks report every evaluation error instead of silently skipping it.

Only #409 broke compilation, in four places: the two template-type
constructors and the two name getters. The Python-facing `name` stays a
`str` holding the raw template text. v0 models these as
AmountCapabilityName / AttributeCapabilityName, both subclasses of v0's
FormatString, which subclasses str -- so exposing an openjd.expr
.FormatString here would make v0 and v1 diverge where they agree, and
.raw() is what #409 prescribes for reading these fields. One behaviour
change falls out of the adaptation rather than upstream: the constructor
parses, so a malformed format string now raises ExpressionError.

create_job needed no code change. The binding derives its context from
job_template.default_validation_context() when the caller passes none,
which covers the template's extensions by construction; a caller-supplied
mismatched context is what #407 now rejects, and the binding surfaces it.

#407 is a squash of five commits whose message documents three behaviour
changes the release changelog and the PR body do not name:

- eval_boolop no longer suppresses budget errors in operands after an
  unresolved one, the same bypass eval_ifexp had.
- A list comprehension over a *concrete* iterable whose filter evaluates
  unresolved concludes unresolved[list[T]] instead of hard-erroring. This
  was a defect that predated the release, masked by the lenient policy
  #407 deleted, and reachable only at job creation -- the one stage where
  the iterable is concrete and the filter is not.
- Template validation and create_job's re-checks evaluate under
  PathFormat::Posix, so validation outcomes no longer depend on the host
  OS. Windows-only in effect; unverified locally.

Verified: 6243 passed / 24 skipped / 3 xfailed, coverage 94.22%. The three
xfails are the pre-existing openjd.expr known gaps; none flipped. ruff,
black, mypy, cargo fmt and clippy clean. Every behaviour change above was
measured through this package's public API against both 0.9.0 and 0.10.0,
and the openjd.expr expectations are taken from the upstream assertions in
test_unresolved_eval.rs and test_memory.rs.

Four mutants, each rebuilt and each caught: reverting the three pins kills
53 of the new cases, the two __repr__ sites kill 2, the two name getters
kill 11, and swallowing the name parse error kills 2. Each was restored
byte-for-byte with the bytecode cache cleared between runs.

THIRD-PARTY-LICENSES.txt moves only the three version lines; the release
pulled in no new transitive crates.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
@leongdl
leongdl requested a review from a team as a code owner September 28, 2026 20:39
AlexTranAmz
AlexTranAmz previously approved these changes Sep 28, 2026
jericht
jericht previously approved these changes Sep 28, 2026
Comment thread rust-bindings/src/model/template_types.rs Outdated
Comment thread specs/python-model-interface.md Outdated
Review feedback on OpenJobDescription#373: consistency with the other v1 fields beats
consistency with v0 here.

openjd-rs#409 made template::AmountRequirement::name and
template::AttributeRequirement::name a FormatString. The first pass kept the
Python-facing `name` a `str` and parsed on the way in, on the grounds that
v0 models these as AmountCapabilityName / AttributeCapabilityName, both
subclasses of v0's FormatString, which subclasses str. That argument looks
at the wrong neighbour: every other FormatString-typed field on the v1
template types -- `min`, `max`, `anyOf`, `allOf`, `Action.command` -- is an
openjd.expr.FormatString and refuses a bare str. `name` now matches them.

This deletes more than it adds. parse_capability_name and its doc comment go
away, because the FormatString arrives already parsed, and both constructors
go back to being infallible. Validation moves to where it belongs: a
malformed name is rejected by FormatString's own constructor rather than by
a str-typed field that parses behind the caller's back. __repr__ keeps
.raw(), matching Action's treatment of its command.

This is a breaking change to the v1 Python API, and two pre-existing tests
prove it: TestPickle::test_amount_requirement and test_attribute_requirement
both passed `name=` a str, and TestStepTemplate::test_host_requirements
compared `.name` to one. All three are updated, so this is a behaviour
change and not a refactor.

Also from review: the spec edit that changed the documented ModelProfile repr
to `revision=v2023_09` was wrong -- PyModelProfile::__repr__ still emits the
uppercase-V form (rust-bindings/src/model/profile.rs:397), measured as
`ModelProfile(revision=V2023_09, extensions=[EXPR])`. Reverted. The two
`SpecificationRevision.v2023_09` corrections in the same block stand: that
repr is lowercase (profile.rs:63) and `V2023_09` is not an attribute at all.
The divergence between the two reprs is pre-existing and left alone here.

Verified: 6245 passed / 24 skipped / 3 xfailed, coverage 94.32%. ruff, black,
mypy, cargo fmt and clippy clean. Four mutants, each rebuilt and each caught:
reverting the pins to 0.9.0 kills 68 cases, formatting the FormatString in
both __repr__ sites kills 2, both getters returning a constant kills 12, and
both constructors discarding the supplied name kills 10.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
@leongdl
leongdl dismissed stale reviews from jericht and AlexTranAmz via 89a8662 September 28, 2026 21:57
Comment thread rust-bindings/src/model/template_types.rs
@leongdl leongdl changed the title chore(deps): Bump openjd-* Rust crates to the 0.10.0 release feat!: Bump openjd-* Rust crates to the 0.10.0 release Sep 28, 2026
@leongdl
leongdl merged commit a2b63c1 into OpenJobDescription:mainline Sep 28, 2026
46 of 59 checks passed
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.

4 participants