From f6a50a172bdf32df02922a91feb8147c4bcdaefb Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Wed, 11 Feb 2026 13:14:05 -0700 Subject: [PATCH 1/9] feat: add Race behavior --- bonsai/src/behavior.rs | 6 ++++++ bonsai/src/lib.rs | 2 +- bonsai/src/state.rs | 17 +++++++++++++++++ bonsai/src/visualizer.rs | 8 ++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/bonsai/src/behavior.rs b/bonsai/src/behavior.rs index 783fbe4..6820f04 100644 --- a/bonsai/src/behavior.rs +++ b/bonsai/src/behavior.rs @@ -128,6 +128,12 @@ pub enum Behavior { /// Succeeds if all behaviors succeed, but only if succeeding in sequence. /// Fails if one behavior fails. After(Vec>), + /// Runs all behaviors in parallel until one completes (succeeds or fails). + /// + /// Returns the status of the first behavior to complete, + /// whether that is `Success` or `Failure`. + /// If all behaviors remain `Running`, returns `Running`. + Race(Vec>), } #[cfg(test)] diff --git a/bonsai/src/lib.rs b/bonsai/src/lib.rs index 0ab85c2..49d9825 100644 --- a/bonsai/src/lib.rs +++ b/bonsai/src/lib.rs @@ -118,7 +118,7 @@ //! ``` pub use behavior::Behavior::{ - self, Action, After, AlwaysSucceed, If, Invert, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While, + self, Action, After, AlwaysSucceed, If, Invert, Race, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While, WhileAll, }; diff --git a/bonsai/src/state.rs b/bonsai/src/state.rs index 2c7365f..a9f6983 100644 --- a/bonsai/src/state.rs +++ b/bonsai/src/state.rs @@ -102,6 +102,8 @@ pub(crate) enum State { /// Keeps track of a `WhenAny` behavior. As the states finish, they are set /// to [`None`]. WhenAny(Vec>>), + /// Keeps track of a `Race` behavior. + Race(Vec>>), /// Keeps track of an `After` behavior. After { /// The index of the next state that must succeed. @@ -166,6 +168,7 @@ impl State { } Behavior::WhenAll(all) => State::WhenAll(all.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::WhenAny(any) => State::WhenAny(any.into_iter().map(|ev| Some(State::new(ev))).collect()), + Behavior::Race(behaviors) => State::Race(behaviors.into_iter().map(|ev| Some(State::new(ev))).collect()), Behavior::After(after_all) => State::After { next_success_index: 0, states: after_all.into_iter().map(State::new).collect(), @@ -404,6 +407,20 @@ impl State { let any = true; when_all(any, upd, cursors, e, f, blackboard) } + (_, &mut Race(ref mut cursors)) => { + // return the result of the first child to complete, + // regardless of whether it succeeds or fails. + for cur in cursors.iter_mut() { + match *cur { + None => {} + Some(ref mut state) => match state.tick(e, blackboard, f) { + (Running, _) => continue, + (status, dt) => return (status, dt), + }, + } + } + RUNNING + } ( _, &mut After { diff --git a/bonsai/src/visualizer.rs b/bonsai/src/visualizer.rs index 1b601c4..727ac63 100644 --- a/bonsai/src/visualizer.rs +++ b/bonsai/src/visualizer.rs @@ -19,6 +19,7 @@ pub(crate) enum NodeType { WhenAll, WhenAny, After, + Race, } impl BT { @@ -125,6 +126,13 @@ impl BT { Self::dfs_recursive(graph, b, node_id) } } + Behavior::Race(behaviors) => { + let node_id = graph.add_node(NodeType::Race); + graph.add_edge(parent_node, node_id, 1); + for b in behaviors { + Self::dfs_recursive(graph, b, node_id) + } + } } } } From 47f9170e4ef109909e1af662138a8fde37cbb613 Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Wed, 11 Feb 2026 13:16:46 -0700 Subject: [PATCH 2/9] tests for Race behavior --- bonsai/tests/behavior_tests.rs | 96 +++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/bonsai/tests/behavior_tests.rs b/bonsai/tests/behavior_tests.rs index fbbe47c..7848c85 100644 --- a/bonsai/tests/behavior_tests.rs +++ b/bonsai/tests/behavior_tests.rs @@ -1,7 +1,7 @@ use crate::behavior_tests::TestActions::{Dec, Inc, LessThan, LessThanRunningSuccess}; use bonsai_bt::{ - Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Select, Sequence, Status::Running, - Success, UpdateArgs, Wait, WaitForever, WhenAll, While, WhileAll, BT, + Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Race, Select, Sequence, + Status::Running, Success, UpdateArgs, Wait, WaitForever, WhenAll, WhenAny, While, WhileAll, BT, }; /// Some test actions. @@ -610,3 +610,95 @@ fn test_repeat_sequence_empty() { // panics because no behaviors... let _state = BT::new(after, ()); } + +#[test] +fn race_returns_first_success() { + let a: i32 = 0; + // Inc succeeds immediately, Wait is still running + let behavior = Race(vec![Action(Inc), Wait(10.0)]); + let mut state = BT::new(behavior, ()); + let (a, s, _) = tick(a, 0.1, &mut state); + assert_eq!(a, 1); + assert_eq!(s, Success); +} + +#[test] +fn race_returns_first_failure() { + let a: i32 = 5; + // LessThan(1) fails immediately since 5 >= 1, Wait is still running + let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]); + let mut state = BT::new(behavior, ()); + let (a, s, _) = tick(a, 0.1, &mut state); + assert_eq!(a, 5); + assert_eq!(s, Failure); +} + +#[test] +fn race_running_until_first_completes() { + let a: i32 = 0; + // Both children are time-based, neither completes on first tick + let behavior = Race(vec![Wait(1.0), Wait(2.0)]); + let mut state = BT::new(behavior, ()); + + // After 0.5s, both still running + let (_a, s, _) = tick(a, 0.5, &mut state); + assert_eq!(s, Running); + + // After another 0.5s (total 1.0s), first Wait completes with Success + let (_a, s, _) = tick(_a, 0.5, &mut state); + assert_eq!(s, Success); +} + +#[test] +fn race_second_child_wins_if_first_is_running() { + let a: i32 = 0; + let behavior = Race(vec![WaitForever, Action(Inc)]); + let mut state = BT::new(behavior, ()); + let (a, s, _) = tick(a, 0.1, &mut state); + assert_eq!(a, 1); + assert_eq!(s, Success); +} + +#[test] +fn race_failure_short_circuits_unlike_when_any() { + // the main difference from WhenAny: + // WhenAny would swallow the failure and keep running. + // Race returns the failure immediately. + let a: i32 = 5; + // LessThan(1) fails immediately (5 >= 1), Wait(10.0) is still running + let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]); + let mut state = BT::new(behavior, ()); + let (a, s, _) = tick(a, 0.1, &mut state); + assert_eq!(s, Failure); + + // for WhenAny: same children, but failure is swallowed + let behavior_any = WhenAny(vec![Action(LessThan(1)), Wait(10.0)]); + let mut state_any = BT::new(behavior_any, ()); + let (_, s_any, _) = tick(a, 0.1, &mut state_any); + assert_eq!(s_any, Running); +} + +#[test] +fn race_timeout_pattern() { + let a: i32 = 0; + // Simulate a "slow action" using WaitForever with a 1-second timeout. + // The timeout (Wait) fires first. + let behavior = Race(vec![WaitForever, Wait(1.0)]); + let mut state = BT::new(behavior, ()); + + let (_, s, _) = tick(a, 0.5, &mut state); + assert_eq!(s, Running); + + let (_, s, _) = tick(a, 0.5, &mut state); + assert_eq!(s, Success); +} + +#[test] +fn race_empty() { + let a: i32 = 0; + let behavior = Race(vec![]); + let mut state = BT::new(behavior, ()); + let (_, s, _) = tick(a, 0.1, &mut state); + // No children means nothing can complete, stays Running + assert_eq!(s, Running); +} From 5600fb22ae2515daf38adec755f9ac6bb5d43746 Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Thu, 12 Mar 2026 14:37:50 -0600 Subject: [PATCH 3/9] add timeout example for Race --- examples/Cargo.toml | 4 + examples/src/race_timeout/main.rs | 119 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 examples/src/race_timeout/main.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index fd94ad5..a04892a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -47,3 +47,7 @@ path = "src/graphviz/main.rs" [[bin]] name = "simple_npc_ai" path = "src/simple_npc_ai/main.rs" + +[[bin]] +name = "race_timeout" +path = "src/race_timeout/main.rs" \ No newline at end of file diff --git a/examples/src/race_timeout/main.rs b/examples/src/race_timeout/main.rs new file mode 100644 index 0000000..e7a5c07 --- /dev/null +++ b/examples/src/race_timeout/main.rs @@ -0,0 +1,119 @@ +use bonsai_bt::Behavior::Wait; +use bonsai_bt::{ + Behavior::Race, Behavior::Action, Behavior::Sequence, + Event, Status, Timer, UpdateArgs, BT, RUNNING, +}; +use futures::FutureExt; +use std::collections::HashMap; +use std::sync::mpsc::{channel, Receiver}; +use std::thread::sleep; +use std::time::Duration; +use tokio::time::sleep as async_sleep; +use rand::Rng; + + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub enum MissionAction { + /// The main job that finishes after a random delay + DoWork, + /// Hard deadline that fires after a fixed delay + OnTimeout, +} + +pub struct MissionState { + pub work: Option>, +} + +/// Simulates a unit of work whose duration is random. +/// Sometimes it finishes before the timeout and sometimes it doesn't. +async fn do_work_task(tx: std::sync::mpsc::Sender) { + let work_ms: u64 = rand::thread_rng().gen_range(200..=1200); + println!("[do_work] started."); + + let step = Duration::from_millis(100); + let mut elapsed = 0u64; + while elapsed < work_ms { + + if tx.send(Status::Running).is_err() { + println!("[do_work] preempted by timeout, stopping."); + return; + } + async_sleep(step).await; + elapsed += step.as_millis() as u64; + } + + println!("[do_work] finished after {elapsed} ms"); + let _ = tx.send(Status::Success); +} + +async fn tick( + timer: &mut Timer, + state: &mut MissionState, + bt: &mut BT>, +) -> std::option::Option<(Status, f64)> { + let dt = timer.get_dt(); + let e: Event = UpdateArgs { dt }.into(); + + bt.tick(&e, &mut |args: bonsai_bt::ActionArgs, _| { + match *args.action { + MissionAction::DoWork => { + if let Some(rx) = &state.work { + match rx.recv() { + Ok(Status::Running) => RUNNING, + Ok(Status::Success) => { + state.work = None; + (Status::Success, args.dt) + } + Ok(Status::Failure) | Err(_) => { + state.work = None; + (Status::Failure, args.dt) + } + } + } else { + let (tx, rx) = channel(); + let (job, handle) = do_work_task(tx).remote_handle(); + handle.forget(); + tokio::spawn(job); + state.work = Some(rx); + match state.work.as_ref().unwrap().recv().unwrap() { + Status::Running => RUNNING, + s => (s, args.dt), + } + } + } + + MissionAction::OnTimeout => { + eprintln!("do_work timed out!"); + (Status::Failure, args.dt) + } + } + }) +} + +#[tokio::main] +async fn main() { + const TIMEOUT_S: f64 = 0.6; + + let behavior = Sequence(vec![ + Race(vec![ + Action(MissionAction::DoWork), + Sequence(vec![ + Wait(TIMEOUT_S), + Action(MissionAction::OnTimeout) + ]) + ]), + ]); + + let mut bt = BT::new(behavior, HashMap::new()); + let mut timer = Timer::init_time(); + let mut state = MissionState { + work: None, + }; + + loop { + sleep(Duration::from_millis(50)); + if tick(&mut timer, &mut state, &mut bt).await.is_none() { + break; + } + } +} \ No newline at end of file From e22821f4f41323235466241118eca284cf726cbb Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Thu, 12 Mar 2026 14:44:54 -0600 Subject: [PATCH 4/9] add section to readme for race timeout example --- docs/resources/images/race_timeout.png | Bin 0 -> 45862 bytes examples/README.md | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 docs/resources/images/race_timeout.png diff --git a/docs/resources/images/race_timeout.png b/docs/resources/images/race_timeout.png new file mode 100644 index 0000000000000000000000000000000000000000..5713e0b04f7fbbc99d9393551f2ed27a958291f4 GIT binary patch literal 45862 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 literal 0 HcmV?d00001 diff --git a/examples/README.md b/examples/README.md index 66af43f..64ff28c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -65,3 +65,13 @@ Compile the behavior tree into a [graphviz](https://graphviz.org/) compatible [D

+ +## Behavior Timeout (example for Race behavior and long-running jobs) + +This simple example shows an example of using the Race behavior to time out a long-running job that takes a random amount of time to complete. + +`cargo run --bin race_timeout` + +

+ +

\ No newline at end of file From 0655e57b67ddc25f9b7717b461c64134d542480c Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Thu, 12 Mar 2026 15:01:08 -0600 Subject: [PATCH 5/9] clipply lint fixes --- bonsai/src/when_all.rs | 1 + examples/Cargo.toml | 2 +- examples/README.md | 2 +- examples/src/race_timeout/main.rs | 47 ++++++++++++------------------ examples/src/simple_npc_ai/main.rs | 6 ++-- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/bonsai/src/when_all.rs b/bonsai/src/when_all.rs index 29503b1..8df34ec 100644 --- a/bonsai/src/when_all.rs +++ b/bonsai/src/when_all.rs @@ -55,6 +55,7 @@ where terminated += 1; *cur = None; } + #[allow(clippy::manual_unwrap_or)] match terminated { // If there are no events, there is a whole 'dt' left. 0 if cursors.is_empty() => ( diff --git a/examples/Cargo.toml b/examples/Cargo.toml index a04892a..7fea124 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -50,4 +50,4 @@ path = "src/simple_npc_ai/main.rs" [[bin]] name = "race_timeout" -path = "src/race_timeout/main.rs" \ No newline at end of file +path = "src/race_timeout/main.rs" diff --git a/examples/README.md b/examples/README.md index 64ff28c..c129b6d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -74,4 +74,4 @@ This simple example shows an example of using the Race behavior to time out a lo

-

\ No newline at end of file +

diff --git a/examples/src/race_timeout/main.rs b/examples/src/race_timeout/main.rs index e7a5c07..5f3bc02 100644 --- a/examples/src/race_timeout/main.rs +++ b/examples/src/race_timeout/main.rs @@ -1,16 +1,14 @@ use bonsai_bt::Behavior::Wait; use bonsai_bt::{ - Behavior::Race, Behavior::Action, Behavior::Sequence, - Event, Status, Timer, UpdateArgs, BT, RUNNING, + Behavior::Action, Behavior::Race, Behavior::Sequence, Event, Float, Status, Timer, UpdateArgs, BT, RUNNING, }; use futures::FutureExt; +use rand::Rng; use std::collections::HashMap; use std::sync::mpsc::{channel, Receiver}; use std::thread::sleep; use std::time::Duration; use tokio::time::sleep as async_sleep; -use rand::Rng; - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub enum MissionAction { @@ -21,7 +19,7 @@ pub enum MissionAction { } pub struct MissionState { - pub work: Option>, + pub work: Option>, } /// Simulates a unit of work whose duration is random. @@ -33,7 +31,6 @@ async fn do_work_task(tx: std::sync::mpsc::Sender) { let step = Duration::from_millis(100); let mut elapsed = 0u64; while elapsed < work_ms { - if tx.send(Status::Running).is_err() { println!("[do_work] preempted by timeout, stopping."); return; @@ -50,17 +47,18 @@ async fn tick( timer: &mut Timer, state: &mut MissionState, bt: &mut BT>, -) -> std::option::Option<(Status, f64)> { - let dt = timer.get_dt(); +) -> std::option::Option<(Status, Float)> { + let dt: Float = timer.get_dt(); let e: Event = UpdateArgs { dt }.into(); - bt.tick(&e, &mut |args: bonsai_bt::ActionArgs, _| { - match *args.action { + bt.tick( + &e, + &mut |args: bonsai_bt::ActionArgs, _| match *args.action { MissionAction::DoWork => { if let Some(rx) = &state.work { match rx.recv() { - Ok(Status::Running) => RUNNING, - Ok(Status::Success) => { + Ok(Status::Running) => RUNNING, + Ok(Status::Success) => { state.work = None; (Status::Success, args.dt) } @@ -86,29 +84,22 @@ async fn tick( eprintln!("do_work timed out!"); (Status::Failure, args.dt) } - } - }) + }, + ) } #[tokio::main] async fn main() { - const TIMEOUT_S: f64 = 0.6; + const TIMEOUT_S: Float = 0.6; - let behavior = Sequence(vec![ - Race(vec![ - Action(MissionAction::DoWork), - Sequence(vec![ - Wait(TIMEOUT_S), - Action(MissionAction::OnTimeout) - ]) - ]), - ]); + let behavior = Sequence(vec![Race(vec![ + Action(MissionAction::DoWork), + Sequence(vec![Wait(TIMEOUT_S), Action(MissionAction::OnTimeout)]), + ])]); let mut bt = BT::new(behavior, HashMap::new()); let mut timer = Timer::init_time(); - let mut state = MissionState { - work: None, - }; + let mut state = MissionState { work: None }; loop { sleep(Duration::from_millis(50)); @@ -116,4 +107,4 @@ async fn main() { break; } } -} \ No newline at end of file +} diff --git a/examples/src/simple_npc_ai/main.rs b/examples/src/simple_npc_ai/main.rs index 937752c..b868d26 100644 --- a/examples/src/simple_npc_ai/main.rs +++ b/examples/src/simple_npc_ai/main.rs @@ -124,12 +124,12 @@ impl EnemyNPCState { /// 6. check if action points are available (yes) /// 7. run /// 8. shoot (notice that we don't have action points -/// here but we try anyway and move on the sequence) +/// here but we try anyway and move on the sequence) /// 9. check if action points are available (no) /// 10. exit the subprogram /// 11. rest and regain action points -/// (this action returns [Running] until fully rested -/// so control flow is returned to main loop) +/// (this action returns [Running] until fully rested +/// so control flow is returned to main loop) /// 12. kill the NPC /// 13. check if the NPC is dead (yes) /// 14. quit From a473b4a4e266dd127c9fc47d2a94e9f0a694cdae Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Tue, 17 Mar 2026 00:13:08 -0600 Subject: [PATCH 6/9] bump version --- bonsai/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonsai/Cargo.toml b/bonsai/Cargo.toml index 242f6fe..f697be9 100644 --- a/bonsai/Cargo.toml +++ b/bonsai/Cargo.toml @@ -12,7 +12,7 @@ name = "bonsai-bt" readme = "../README.md" repository = "https://github.com/sollimann/bonsai.git" rust-version = "1.80.0" -version = "0.10.0" +version = "0.11.0" [lib] name = "bonsai_bt" From eed2b90ae8e460a5878fe9d1a5bb1e85dd6902a6 Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Tue, 17 Mar 2026 00:19:33 -0600 Subject: [PATCH 7/9] make note of new Race behavior in the docs --- README.md | 1 + bonsai/src/lib.rs | 1 + docs/concepts/README.md | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index c363987..729cee6 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ For example, if you have a state `A` and a state `B`: - Do `A`, `B` repeatedly while `LoopCondition` runs: `WhileAll(LoopCondition, [A, B])`. After *All* nodes `A`, `B` are completed successfully, check the condition node. - Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])` - Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])` +- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])` - Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])` See the `Behavior` enum for more information. diff --git a/bonsai/src/lib.rs b/bonsai/src/lib.rs index 49d9825..9e92716 100644 --- a/bonsai/src/lib.rs +++ b/bonsai/src/lib.rs @@ -24,6 +24,7 @@ //! - Do `A`, `B` forever: `While(WaitForever, [A, B])` //! - Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])` //! - Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])` +//! - Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])` //! - Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])` //! //! See the `Behavior` enum for more information. diff --git a/docs/concepts/README.md b/docs/concepts/README.md index 51f73d6..b87c336 100644 --- a/docs/concepts/README.md +++ b/docs/concepts/README.md @@ -56,6 +56,7 @@ For example, if you have a state `A` and a state `B`: - Do `A`, `B` forever: `While(WaitForever, [A, B])` - Run `A` and `B` in parallel and wait for both to succeed: `WhenAll([A, B])` - Run `A` and `B` in parallel and wait for any to succeed: `WhenAny([A, B])` +- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])` - Run `A` and `B` in parallel, but `A` has to succeed before `B`: `After([A, B])` See the `Behavior` enum for more information. From f5df247d8bda3ec18793f4d499934a53f331380f Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Tue, 17 Mar 2026 00:24:03 -0600 Subject: [PATCH 8/9] update precommit version --- .github/workflows/rust-pr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index 87cc8f2..da09923 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -13,9 +13,9 @@ jobs: steps: - name: Install libudev run: sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 build: runs-on: ubuntu-latest steps: From a6614a9e7559a83a3ecf5e9c3dfae1d256eb6baa Mon Sep 17 00:00:00 2001 From: Matthew Ashton Knochel Date: Tue, 17 Mar 2026 00:25:30 -0600 Subject: [PATCH 9/9] update checkout and setup python actions to latest version --- .github/workflows/rust-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index da09923..5026a5f 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -13,8 +13,8 @@ jobs: steps: - name: Install libudev run: sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-python@v6.2.0 - uses: pre-commit/action@v3.0.1 build: runs-on: ubuntu-latest