git-ents.gitmain
⌘K
foforge
lib.rs155 lines · 6.9 KB · rusthistorycomment on this file
1//! Effect execution, results, and toolchains at run time (`docs/spec/effect.adoc`):
2//! the `Executor` trait, its Docker and Sprite backends, toolchain
3//! materialization, and the run loop that ties them to
4//! [`ents_receive::receive`] as the sole path a result re-enters the
5//! repository.
6//!
7//! This crate closes the loop `ents-gate` and `ents-receive` open
8//! (`docs/abstractions.adoc`, "The loop"): an effect's trigger is
9//! evaluated by `ents-query` (already linked by `ents-receive` for
10//! footprint matching, never by this crate's own dependents in the other
11//! direction — `arch.query-effect-split`), its run happens behind one
12//! [`Executor`] seam with multiple backends, and its outcome returns as an
13//! ordinary signed commit through [`write_result`], a `receive` client
14//! exactly like the CLI or a web edit.
15//!
16//! # Spec coverage
17//!
18//! From `docs/spec/effect.adoc`:
19//!
20//! - `effect.definition`, `effect.admin-only` — already carried by
21//! `ents-model`'s [`ents_model::Effect`] and `ents-gate`'s default
22//! authorization arm; nothing new here.
23//! - `effect.validation` — [`definition::validate`]. `ents-receive` cannot
24//! call this itself (`arch.query-effect-split`); a future frontend that
25//! builds an effect-definition commit does, before ever proposing the
26//! write.
27//! - `effect.execution`, `effect.deployment-property` — [`Executor`],
28//! [`SandboxInputs`], [`RunOutput`]; [`docker::DockerExecutor`] (feature
29//! `docker`), [`sprite::SpriteExecutor`] (feature `sprite`),
30//! [`UnsandboxedExecutor`]. No executor, sandbox, or retry choice is
31//! readable from an [`ents_model::Effect`] — every backend is
32//! constructed and selected only by a composition root.
33//! - `effect.local-run` — [`run::run_one`] is the single code path
34//! [`run::run_effect`] (the boot-time/on-demand form, no queue) and a
35//! future hosted worker (a queue drain feeding the same [`run::run_one`]
36//! calls) both use.
37//! - `effect.results-writeback`, `effect.result-taxonomy` —
38//! [`write_result`]: an ordinary [`ents_receive::receive`] client,
39//! landing exactly `pass`/`fail`/`error` on
40//! `refs/meta/results/<effect>/<short-oid>`
41//! ([`run::short_oid`]). This crate never writes `Status::Error` itself
42//! — see [`Error`]'s own doc for why an infrastructure failure is always
43//! an `Err`, never a taxonomy value this crate chooses on a caller's
44//! behalf.
45//! - `effect.identity` — [`write_result`] takes a `sign` closure the
46//! composition root injects (mirrors `ents_sync::resolve::merge_heads`);
47//! this crate never holds key material.
48//! - `effect.official` — a refname-authorization rule on canonical
49//! `refs/meta/results/<effect>/*`, owned by `ents-gate`'s future
50//! Config-driven worker-key narrowing (see that crate's own doc); this
51//! crate only chooses *which* refname to target
52//! ([`run::run_one`]'s `results_ref`), never judges authorization.
53//! - `effect.self-run` — [`write_result`] and [`run::run_effect`] accept
54//! any results refname, canonical or
55//! [`ents_model::namespace::self_result_ref`]; adopting a self-run
56//! result onto the canonical ref is `ents-sync`'s adoption merge
57//! (`gate.adoption-merge`, `sync.adoption-machinery`), unchanged by this
58//! crate.
59//! - `effect.toolchains`, `model.toolchain` — moved to `ents-kiln`
60//! (`Toolchain`, `Recipe`, `Component`, `toolchain::resolve`,
61//! `toolchain::materialize`): this crate's own contract is now just that
62//! [`SandboxInputs::toolchains`] is a pre-materialized slice; resolving
63//! declared names to that slice is the composition root's job, done via
64//! `ents-kiln`. Only [`Executor::run`]'s sandbox ever touches the
65//! materialized bytes this crate hands it.
66//! - `effect.fanout-index` — structurally satisfied, no dedicated code: a
67//! fanout-index rebuild is an ordinary effect (`run`ning `git index
68//! rebuild` or similar, a later, unbuilt command), so it uses exactly
69//! the same [`Executor`] and [`write_result`] path as any other effect.
70//!
71//! # Examples
72//!
73//! An end-to-end local run: enroll a worker, define an effect and a
74//! (trivial, embedded-empty) toolchain, advance a code ref, and run the
75//! effect with a stub executor — the shape `effect.local-run` names, minus
76//! only a real sandbox.
77//!
78//! ```
79//! use ents_effect::run::{run_effect, short_oid};
80//! use ents_effect::{Executor, RunOutput, RunStatus, SandboxInputs};
81//! use ents_model::{Effect, Provenance, namespace};
82//! use ents_receive::{Mode, NullEventSink};
83//! use ents_testutil::{Keypair, MemRefStore, ObjectStore, advance_ref, enroll_member};
84//! use gix_ref_store::RefStoreRead as _;
85//!
86//! struct AlwaysPass;
87//! impl Executor for AlwaysPass {
88//! fn run(&self, _inputs: &SandboxInputs<'_>) -> ents_effect::Result<RunOutput> {
89//! Ok(RunOutput { status: RunStatus::Pass, log: "ok".into() })
90//! }
91//! }
92//!
93//! let refs = MemRefStore::default();
94//! let objects = ObjectStore::default();
95//! let worker = Keypair::from_seed(1);
96//! enroll_member(&refs, &objects, "worker", &worker, Provenance::AdminRegistered, 100);
97//!
98//! // No declared toolchains: resolving names to materialized directories
99//! // is the composition root's job (via `ents-kiln`), not this crate's —
100//! // `run_effect` only ever receives an already-materialized slice.
101//! let effect = Effect {
102//! name: "unit".into(),
103//! trigger: "rev(refs/heads/main)".into(),
104//! toolchains: vec![],
105//! run: "true".into(),
106//! };
107//! assert!(ents_effect::definition::validate(&effect).is_ok());
108//!
109//! let commits = advance_ref(&refs, &objects, "refs/heads/main", 1, 200);
110//!
111//! let author = gix::actor::Signature {
112//! name: "worker".into(), email: "worker@ents.test".into(),
113//! time: gix::date::Time { seconds: 300, offset: 0 },
114//! };
115//! let scratch = tempfile::tempdir().expect("tempdir");
116//!
117//! let outcomes = run_effect(
118//! &refs, &objects, &NullEventSink, &AlwaysPass, scratch.path(), &[],
119//! "unit", &effect, None,
120//! |short| Ok(namespace::result_ref("unit", short).expect("valid")),
121//! &author, &|payload| worker.sign(payload), Mode::Advisory,
122//! ).expect("runs");
123//!
124//! assert_eq!(outcomes.len(), 1);
125//! assert_eq!(outcomes[0].0, commits[0]);
126//! assert_eq!(outcomes[0].1.result, ents_receive::TxResult::Applied);
127//!
128//! // The canonical results ref now carries a pass.
129//! let name = namespace::result_ref("unit", &short_oid(commits[0])).expect("valid");
130//! assert!(refs.get(name.as_ref()).expect("readable").is_some());
131//! ```
132
133pub mod definition;
134mod error;
135pub mod executor;
136pub mod materialize;
137mod results;
138pub mod run;
139mod unsandboxed;
140
141#[cfg(feature = "docker")]
142pub mod docker;
143#[cfg(feature = "sprite")]
144pub mod sprite;
145
146pub use error::{Error, Result};
147pub use executor::{Executor, RunOutput, RunStatus, SandboxInputs};
148pub use results::write_result;
149pub use run::{run_effect, run_one};
150pub use unsandboxed::UnsandboxedExecutor;
151
152#[cfg(feature = "docker")]
153pub use docker::DockerExecutor;
154#[cfg(feature = "sprite")]
155pub use sprite::SpriteExecutor;