repolith_core/action.rs
1use crate::types::{ActionId, BuildError, BuildOutput, Ctx, Sha256};
2use async_trait::async_trait;
3
4/// Represents an executable action within the repolith graph.
5///
6/// Actions are the fundamental units of work. They declare their dependencies,
7/// compute their input state, and execute their logic.
8///
9/// Note: Long-running `execute` implementations should periodically poll
10/// `ctx.cancel.is_cancelled()` to support graceful termination.
11#[async_trait]
12pub trait Action: Send + Sync {
13 /// Returns the unique identifier for this action.
14 fn id(&self) -> ActionId;
15
16 /// Returns the list of action IDs that this action depends on.
17 fn deps(&self) -> Vec<ActionId>;
18
19 /// Computes the hash representing the input state of this action.
20 /// This is used for caching and determining if the action needs to run.
21 async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError>;
22
23 /// Executes the action's core logic.
24 async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError>;
25
26 /// `true` for actions that only *coordinate* other actions (federation's
27 /// `RepolithSync`) rather than doing bounded work themselves.
28 ///
29 /// The orchestrator does **not** charge a concurrency permit for
30 /// coordinators: the child actions they spawn acquire from the same
31 /// shared semaphore, so charging the coordinator too would deadlock at
32 /// `--jobs 1` (the coordinator would hold the only permit while its
33 /// children wait for it).
34 fn is_coordinator(&self) -> bool {
35 false
36 }
37
38 /// Whether the artifact this action produces is still present.
39 ///
40 /// A cached `Success` only means "these inputs were built **somewhere,
41 /// once**" — not that the result is still on *this* machine. Without
42 /// this probe the planner trusts the cache blindly and reports
43 /// `up to date` when the binary was deleted, or when the cache entry
44 /// was written by a different machine sharing a backend (see the
45 /// Neo4j backend). Actions that leave a persistent artifact should
46 /// override this; the default `true` suits actions with nothing to
47 /// check (coordinators, pure side effects).
48 ///
49 /// Implementations must be **cheap** — this runs for every action on
50 /// every plan. A `stat` or a single subprocess, nothing more. On any
51 /// doubt return `true`: a false negative causes a needless rebuild,
52 /// while a false positive resurrects the silent-staleness bug.
53 async fn output_present(&self, ctx: &Ctx) -> bool {
54 let _ = ctx;
55 true
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 struct MockAction;
64
65 #[async_trait]
66 impl Action for MockAction {
67 fn id(&self) -> ActionId {
68 ActionId("mock".to_string())
69 }
70
71 fn deps(&self) -> Vec<ActionId> {
72 vec![]
73 }
74
75 async fn input_hash(&self, _ctx: &Ctx) -> Result<Sha256, BuildError> {
76 Ok(Sha256([0; 32]))
77 }
78
79 async fn execute(&self, _ctx: &Ctx) -> Result<BuildOutput, BuildError> {
80 Ok(BuildOutput {
81 output_hash: Sha256([1; 32]),
82 stdout: String::new(),
83 })
84 }
85 }
86
87 #[test]
88 fn test_action_is_object_safe() {
89 // This line will fail to compile if `Action` is not object-safe.
90 let _action: Box<dyn Action> = Box::new(MockAction);
91 }
92}