feat: carry the execution plan over the wire #3

Merged
day01 merged %!d() commits from %!s() into %!s() 2026-08-05 15:31:59 +00:00
Owner

First half of the slice that runs a job through the control plane: without
this the plan has no way onto the wire, so it lands with the protocol rather
than ahead of it.

The plan travels as itself

D018 settled that the protocol carries the compiled plan, not a parallel set
of DTOs. The plan model therefore carries serde and check-architecture.sh
stops rejecting it — while still rejecting serde_json, prost, tokio,
reqwest, gix and bollard, because choosing an encoding and owning a
transport remain someone else's job.

Types that validate on construction deserialize through those constructors:
identifiers via TryFrom<String>, PositiveDuration as whole minutes, and
RunnerSelection through try_new, so a plan cannot arrive naming neither a
group nor a label. RunnerSelection serializes by reference rather than
through Into, which keeps a Clone bound out of every node holding one.

Expressions

ExpressionProgram now keeps the source it parsed and renders it through
Display. A condition that inherits an implicit status check rewrites the
source (success() && (…)) instead of splicing nodes, so the program still
parses back to exactly the tree it carries. Serialization is that source; no
syntax tree reaches the wire.

Schema version

VersionedPlan pairs a plan with PlanSchemaVersion, which decodes only the
version this build speaks and fails with PlanSchemaError::Unsupported
otherwise. Internal contract for 0.3, versioned from the first day per D018.

Verification

cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings,
cargo test --workspace --all-targets --all-features, check-architecture.sh,
check-rust-loc.sh.

tests/plan_wire.rs compiles a workflow with a container, a matrix strategy, a
timeout, an if:, an env template and an action step, then asserts the plan
round-trips unchanged, that the schema version is on the wire and an unknown one
is refused, that the condition source appears while no syntax tree does, and
that a forged job key fails to decode.

First half of the slice that runs a job through the control plane: without this the plan has no way onto the wire, so it lands with the protocol rather than ahead of it. ## The plan travels as itself `D018` settled that the protocol carries the compiled plan, not a parallel set of DTOs. The plan model therefore carries `serde` and `check-architecture.sh` stops rejecting it — while still rejecting `serde_json`, `prost`, `tokio`, `reqwest`, `gix` and `bollard`, because choosing an encoding and owning a transport remain someone else's job. Types that validate on construction deserialize through those constructors: identifiers via `TryFrom<String>`, `PositiveDuration` as whole minutes, and `RunnerSelection` through `try_new`, so a plan cannot arrive naming neither a group nor a label. `RunnerSelection` serializes by reference rather than through `Into`, which keeps a `Clone` bound out of every node holding one. ## Expressions `ExpressionProgram` now keeps the source it parsed and renders it through `Display`. A condition that inherits an implicit status check rewrites the source (`success() && (…)`) instead of splicing nodes, so the program still parses back to exactly the tree it carries. Serialization is that source; no syntax tree reaches the wire. ## Schema version `VersionedPlan` pairs a plan with `PlanSchemaVersion`, which decodes only the version this build speaks and fails with `PlanSchemaError::Unsupported` otherwise. Internal contract for 0.3, versioned from the first day per D018. ## Verification `cargo fmt --check`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, `cargo test --workspace --all-targets --all-features`, `check-architecture.sh`, `check-rust-loc.sh`. `tests/plan_wire.rs` compiles a workflow with a container, a matrix strategy, a timeout, an `if:`, an env template and an action step, then asserts the plan round-trips unchanged, that the schema version is on the wire and an unknown one is refused, that the condition source appears while no syntax tree does, and that a forged job key fails to decode.
day01 added 1 commit 2026-08-05 06:06:26 +00:00
		The plan model gains serde, so the plan the control plane compiles is the plan
the runner decodes rather than a copy of it converted at a boundary. Validated
types deserialize through their own constructors, expressions travel as source
and are rebuilt by parsing, and VersionedPlan refuses a schema version this
build does not speak.
	
day01 added 1 commit 2026-08-05 06:28:11 +00:00
		A plan that travels needs the same two things a workflow file needs: which
dialect produced it and the bytes. Keeping the payload opaque lets a plan be
moved without knowing which expression type it holds.
	
day01 added 1 commit 2026-08-05 06:30:24 +00:00
		domain_map! is exported and used by consumers whose maps hold types that do not
serialize. Only DynamicObject travels inside a plan, so only DynamicObject gets
the impls.
	
day01 added 1 commit 2026-08-05 06:48:34 +00:00
day01 added 1 commit 2026-08-05 10:39:23 +00:00
		dtolnay/rust-toolchain publishes versions as branches whose heads move, so a
commit pinned today is unreachable from any ref once the branch advances and a
fresh mirror no longer carries it. The branch is what a mirror can serve.
	
day01 added 1 commit 2026-08-05 10:42:42 +00:00
		The runner on production cannot resolve an action reference named like a
version, so dtolnay/rust-toolchain is unusable there until it ships the fix.
The toolchain is pinned by image digest instead, which the job container
resolves without any action at all.
	
day01 added 1 commit 2026-08-05 12:11:44 +00:00
		The compiler accepted only an already-specialised matrix, because the forge
expanded it before the runner ever saw it. The control plane compiles the
workflow as written, so a dimension now holds all its values and expansion
turns one plan into one plan per combination.

A node is still only ever handed a plan with a single value per dimension:
expansion happens before assignment, never on the node.
	
day01 merged commit 3336ee3889 into %!s() 2026-08-05 15:31:59 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: syncode/workflow#3