Compare commits

...

27 Commits

Author SHA1 Message Date
c7f4f8e7e6 fix(deps): update all dependencies 2025-12-03 01:28:17 +00:00
ce444a3f6f chore(deps): update tokio-tracing monorepo 2025-11-29 01:32:38 +00:00
b26dd9f41d chore(deps): update rust crate tracing-subscriber to v0.3.20 2025-11-13 02:57:46 +01:00
f2e6a48c9e chore(deps): update rust crate serde to v1.0.228 2025-11-13 01:57:41 +00:00
b9034de8a0 chore(deps): update rust crate serde to v1.0.219
All checks were successful
forest-ci/push push
2025-04-19 03:25:32 +00:00
d0227cc9e5 chore(deps): update rust crate clap to v4.5.37
All checks were successful
forest-ci/push push
2025-04-19 00:25:46 +00:00
f54b1b870a chore(deps): update all dependencies
All checks were successful
forest-ci/push push
2025-04-15 03:24:26 +00:00
b3d6862195 fix(deps): update all dependencies
All checks were successful
forest-ci/push push
2025-04-15 00:28:34 +00:00
045fc90364 feat: add forestci 2025-04-13 21:16:33 +02:00
91fe491751 chore: minor cleanup 2025-03-23 22:21:39 +01:00
e9e80abad0 feat: plans in workspace now works 2025-03-23 22:20:43 +01:00
1fda414e05 feat: can execute all subcommands from workspace 2025-03-23 21:52:29 +01:00
28a1d09974 feat: can load project files 2025-03-23 20:36:40 +01:00
dca625af31 feat: can load workspace members as array 2025-03-23 20:21:30 +01:00
bd927840d6 feat: implement basic workspace 2025-03-20 08:19:18 +01:00
f98b48667c fix: small bugs in running scripts and set default log level 2025-03-03 23:22:31 +01:00
3510750c55 feat: correct cache timing 2025-02-28 23:41:17 +01:00
0dd768af9d feat: implement cache 2025-02-28 23:40:45 +01:00
ffb81a5ee6 refactor: move git into own file 2025-02-28 23:12:56 +01:00
dfe2ac62e3 feat: can clone using git 2025-02-28 23:08:40 +01:00
f10a1c0ac5 docs: cross of todos 2025-02-28 22:13:02 +01:00
b0261de87e feat: scripts can run from plan 2025-02-28 22:08:56 +01:00
2fadde0f8a refactor: shell 2025-02-28 21:23:23 +01:00
9be64b74b2 feat: implement template plan 2025-02-28 21:21:34 +01:00
ef8deadfd8 feat: is able to call a script from a project 2025-02-27 22:02:58 +01:00
9b6996c261 feat: enable templating 2025-02-27 17:19:56 +01:00
dcf459462e feat: update forest deps 2025-02-27 01:43:33 +01:00
36 changed files with 1648 additions and 431 deletions

2
.env
View File

@@ -3,3 +3,5 @@ FOREST_S3_BUCKET=forest
FOREST_S3_REGION=eu-west-1 FOREST_S3_REGION=eu-west-1
FOREST_S3_USER=forestadmin FOREST_S3_USER=forestadmin
FOREST_S3_PASSWORD=forestadmin FOREST_S3_PASSWORD=forestadmin
FOREST_LOG_LEVEL=forest=trace

View File

@@ -0,0 +1,41 @@
name: Build Forest
on:
- push
- pull_request
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
build:
env:
RUSTFLAGS: -D warnings
timeout_minutes: 30
steps:
- name: Build application
uses: rustlang/rust:nightly
run:
- export SQLX_OFFLINE=true
- cargo build --release
- name: Run tests
uses: rustlang/rust:nightly
run:
- cargo test
- name: Check code formatting
uses: rustlang/rust:nightly
run:
- cargo fmt -- --check
continue_on_error: true
- name: Run clippy lints
uses: rustlang/rust:nightly
run:
- rustup component add clippy
- cargo clippy -- -D warnings
continue_on_error: true

546
Cargo.lock generated
View File

@@ -3,20 +3,14 @@
version = 4 version = 4
[[package]] [[package]]
name = "addr2line" name = "aho-corasick"
version = "0.24.2" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [ dependencies = [
"gimli", "memchr",
] ]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@@ -69,9 +63,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.95" version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@@ -79,21 +73,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -115,6 +94,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.0" version = "1.10.0"
@@ -129,9 +114,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.30" version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -139,9 +124,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.30" version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -151,9 +136,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.28" version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -203,15 +188,6 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -264,9 +240,9 @@ dependencies = [
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
] ]
@@ -290,20 +266,14 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi 0.13.3+wasi-0.2.2", "wasi 0.13.3+wasi-0.2.2",
"windows-targets", "windows-targets 0.52.6",
] ]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -440,9 +410,9 @@ dependencies = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [ dependencies = [
"idna_adapter", "idna_adapter",
"smallvec", "smallvec",
@@ -472,14 +442,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "kdl" name = "jiff"
version = "6.3.4" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661358400b02cbbf1fbd05f0a483335490e8a6bd1867620f2eeb78f304a22f" checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "kdl"
version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
dependencies = [ dependencies = [
"miette", "miette",
"num", "num",
"thiserror",
"winnow", "winnow",
] ]
@@ -491,15 +494,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.4" version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -513,9 +516,18 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
@@ -535,45 +547,23 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "miette" name = "miette"
version = "7.5.0" version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"miette-derive",
"thiserror",
"unicode-width", "unicode-width",
] ]
[[package]]
name = "miette-derive"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "minijinja" name = "minijinja"
version = "2.7.0" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff7b8df5e85e30b87c2b0b3f58ba3a87b68e133738bf512a7713769326dbca9" checksum = "0adbe6e92a6ce0fd6c4aac593fdfd3e3950b0f61b1a63aa9731eb6fd85776fa3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.3" version = "1.0.3"
@@ -587,12 +577,11 @@ dependencies = [
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"overload", "windows-sys 0.59.0",
"winapi",
] ]
[[package]] [[package]]
@@ -628,12 +617,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@@ -674,27 +657,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.3" version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.3"
@@ -715,14 +683,14 @@ dependencies = [
"libc", "libc",
"redox_syscall", "redox_syscall",
"smallvec", "smallvec",
"windows-targets", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
@@ -731,10 +699,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "powerfmt" name = "portable-atomic"
version = "0.2.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
@@ -747,9 +724,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.2" version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@@ -766,34 +743,51 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.8" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
[[package]] [[package]]
name = "rustc-demangle" name = "regex-automata"
version = "0.1.24" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "rusty-s3" name = "rusty-s3"
version = "0.7.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f51a5a6b15f25d3e10c068039ee13befb6110fcb36c2b26317bcbdc23484d96" checksum = "fac2edd2f0b56bd79a7343f49afc01c2d41010df480538a510e0abc56044f66c"
dependencies = [ dependencies = [
"base64", "base64",
"hmac", "hmac",
"jiff",
"md-5", "md-5",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"time",
"url", "url",
"zeroize", "zeroize",
] ]
@@ -821,18 +815,28 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -841,14 +845,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.138" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
"serde", "serde",
"serde_core",
] ]
[[package]] [[package]]
@@ -888,12 +893,12 @@ checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.8" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -936,26 +941,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.8" version = "1.1.8"
@@ -966,37 +951,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@@ -1009,11 +963,10 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.43.0" version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [ dependencies = [
"backtrace",
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
@@ -1022,14 +975,14 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1038,9 +991,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.41" version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@@ -1050,9 +1003,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1061,9 +1014,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.33" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -1082,14 +1035,18 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.19" version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [ dependencies = [
"matchers",
"nu-ansi-term", "nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"thread_local", "thread_local",
"tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
] ]
@@ -1102,9 +1059,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@@ -1114,13 +1071,14 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]
@@ -1143,11 +1101,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.13.2" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"js-sys",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1188,20 +1148,49 @@ dependencies = [
] ]
[[package]] [[package]]
name = "winapi" name = "wasm-bindgen"
version = "0.3.9" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [ dependencies = [
"winapi-i686-pc-windows-gnu", "cfg-if",
"winapi-x86_64-pc-windows-gnu", "once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "winapi-i686-pc-windows-gnu" name = "wasm-bindgen-macro"
version = "0.4.0" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
@@ -1213,10 +1202,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "windows-link"
version = "0.4.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
@@ -1224,7 +1213,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -1233,7 +1222,25 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
] ]
[[package]] [[package]]
@@ -1242,14 +1249,31 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu", "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm", "windows_i686_gnullvm 0.52.6",
"windows_i686_msvc", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
] ]
[[package]] [[package]]
@@ -1258,48 +1282,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.24" version = "0.6.24"
@@ -1362,18 +1434,18 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]
[[package]] [[package]]
name = "zerofrom-derive" name = "zerofrom-derive"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -7,8 +7,8 @@ resolver = "2"
anyhow = { version = "1" } anyhow = { version = "1" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3.18" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env", "cargo", "string"] }
dotenvy = { version = "0.15" } dotenvy = { version = "0.15" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
uuid = { version = "1.7", features = ["v4"] } uuid = { version = "1.7", features = ["v4"] }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "forest" name = "forest"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
@@ -13,11 +13,11 @@ dotenvy.workspace = true
serde.workspace = true serde.workspace = true
uuid.workspace = true uuid.workspace = true
rusty-s3 = "0.7.0" rusty-s3 = "0.8"
url = "2.5.4" url = "2.5"
kdl = "6.3.3" kdl = "6.3"
walkdir = "2.5.0" walkdir = "2.5"
minijinja = "2.7.0" minijinja = "2.7"
glob = "0.3.2" glob = "0.3"
serde_json = "1.0.138" serde_json = "1"
colored_json = "5.0.0" colored_json = "5"

View File

@@ -1,39 +1,26 @@
use std::{net::SocketAddr, path::PathBuf}; use std::{net::SocketAddr, path::PathBuf};
use clap::{Parser, Subcommand}; use anyhow::Context as AnyContext;
use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version};
use colored_json::ToColoredJson; use colored_json::ToColoredJson;
use kdl::KdlDocument; use kdl::KdlDocument;
use rusty_s3::{Bucket, Credentials, S3Action}; use rusty_s3::{Bucket, Credentials, S3Action};
use tokio::io::AsyncWriteExt;
use crate::{ use crate::{
model::{Context, Plan, Project, TemplateType}, model::{Context, ForestFile, Plan, Project, WorkspaceProject},
plan_reconciler::PlanReconciler, plan_reconciler::PlanReconciler,
state::SharedState, state::SharedState,
}; };
#[derive(Parser)] mod run;
#[command(author, version, about, long_about = None, subcommand_required = true)] mod template;
struct Command {
#[command(subcommand)]
command: Option<Commands>,
#[arg(
env = "FOREST_PROJECT_PATH",
long = "project-path",
default_value = "."
)]
project_path: PathBuf,
}
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
Init {}, Init {},
Template(template::Template),
Template {},
Info {}, Info {},
Clean {},
Serve { Serve {
#[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")] #[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")]
host: SocketAddr, host: SocketAddr,
@@ -55,10 +42,34 @@ enum Commands {
}, },
} }
pub async fn execute() -> anyhow::Result<()> { fn get_root(include_run: bool) -> clap::Command {
let cli = Command::parse(); let mut root_cmd = clap::Command::new("forest")
.subcommand_required(true)
.author(crate_authors!())
.version(crate_version!())
.about(crate_description!())
.ignore_errors(include_run)
.arg(
clap::Arg::new("project_path")
.long("project-path")
.env("FOREST_PROJECT_PATH")
.default_value("."),
);
let project_path = &cli.project_path.canonicalize()?; if include_run {
root_cmd = root_cmd.subcommand(clap::Command::new("run").allow_external_subcommands(true))
}
Commands::augment_subcommands(root_cmd)
}
pub async fn execute() -> anyhow::Result<()> {
let matches = get_root(true).get_matches();
let project_path = PathBuf::from(
&matches
.get_one::<String>("project_path")
.expect("project path always to be set"),
)
.canonicalize()?;
let project_file_path = project_path.join("forest.kdl"); let project_file_path = project_path.join("forest.kdl");
if !project_file_path.exists() { if !project_file_path.exists() {
anyhow::bail!( anyhow::bail!(
@@ -68,154 +79,262 @@ pub async fn execute() -> anyhow::Result<()> {
} }
let project_file = tokio::fs::read_to_string(&project_file_path).await?; let project_file = tokio::fs::read_to_string(&project_file_path).await?;
let project_doc: KdlDocument = project_file.parse()?; let doc: KdlDocument = project_file.parse()?;
let project: ForestFile = doc.try_into()?;
let project: Project = project_doc.try_into()?; match project {
tracing::trace!("found a project name: {}", project.name); ForestFile::Workspace(workspace) => {
tracing::trace!("running as workspace");
let plan = if let Some(plan_file_path) = PlanReconciler::new() // 1. For each member load the project
.reconcile(&project, project_path)
.await?
{
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
let plan_doc: KdlDocument = plan_file.parse()?;
let plan: Plan = plan_doc.try_into()?; let mut workspace_members = Vec::new();
tracing::trace!("found a plan name: {}", project.name);
Some(plan) for member in workspace.members {
} else { let workspace_member_path = project_path.join(&member.path);
None
};
let context = Context { project, plan }; let project_file_path = workspace_member_path.join("forest.kdl");
if !project_file_path.exists() {
anyhow::bail!(
"no 'forest.kdl' file was found at: {}",
workspace_member_path.display().to_string()
);
}
match cli.command.unwrap() { let project_file = tokio::fs::read_to_string(&project_file_path).await?;
Commands::Init {} => { let doc: KdlDocument = project_file.parse()?;
tracing::info!("initializing project"); let project: WorkspaceProject = doc.try_into().context(format!(
tracing::trace!("found context: {:?}", context); "workspace member: {} failed to parse",
} &member.path
))?;
Commands::Info {} => { workspace_members.push((workspace_member_path, project));
let output = serde_json::to_string_pretty(&context)?; }
println!("{}", output.to_colored_json_auto().unwrap_or(output));
}
Commands::Template {} => { // TODO: 1a (optional). Resolve dependencies
tracing::info!("templating"); // 2. Reconcile plans
let Some(template) = context.project.templates else { let mut member_contexts = Vec::new();
return Ok(());
};
match template.ty { for (member_path, member) in &workspace_members {
TemplateType::Jinja2 => { match member {
for entry in glob::glob(&format!( WorkspaceProject::Plan(_plan) => {
"{}/{}", tracing::warn!("skipping reconcile for plans for now")
project_path.display().to_string().trim_end_matches("/"), }
template.path.trim_start_matches("./"), WorkspaceProject::Project(project) => {
)) let plan = if let Some(plan_file_path) = PlanReconciler::new()
.map_err(|e| anyhow::anyhow!("failed to read glob pattern: {}", e))? .reconcile(
{ project,
let entry = member_path,
entry.map_err(|e| anyhow::anyhow!("failed to read path: {}", e))?; Some(workspace_members.as_ref()),
let entry_name = entry.display().to_string(); Some(&project_path),
)
.await?
{
let plan_file = tokio::fs::read_to_string(&plan_file_path)
.await
.context(format!(
"failed to read file at: {}",
project_path.to_string_lossy()
))?;
let plan_doc: KdlDocument = plan_file.parse()?;
let entry_rel = if entry.is_absolute() { let plan: Plan = plan_doc.try_into()?;
entry.strip_prefix(project_path).map(|e| e.to_path_buf()) tracing::trace!("found a plan name: {}", project.name);
Some(plan)
} else { } else {
Ok(entry.clone()) None
}; };
let rel_file_path = entry_rel let context = Context {
.map(|p| { project: project.clone(),
if p.file_name() plan,
.map(|f| f.to_string_lossy().ends_with(".jinja2")) };
.unwrap_or(false) member_contexts.push((member_path, context));
{
p.with_file_name(
p.file_stem().expect("to be able to find a filename"),
)
} else {
p.to_path_buf()
}
})
.map_err(|e| {
anyhow::anyhow!(
"failed to find relative file: {}, project: {}, file: {}",
e,
project_path.display(),
entry_name
)
})?;
let output_file_path = project_path
.join(".forest/temp")
.join(&template.output)
.join(rel_file_path);
let contents = tokio::fs::read_to_string(&entry).await.map_err(|e| {
anyhow::anyhow!(
"failed to read template: {}, err: {}",
entry.display(),
e
)
})?;
let mut env = minijinja::Environment::new();
env.add_template(&entry_name, &contents)?;
let tmpl = env.get_template(&entry_name)?;
let output = tmpl
.render(minijinja::context! {})
.map_err(|e| anyhow::anyhow!("failed to render template: {}", e))?;
tracing::info!("rendered template: {}", output);
if let Some(parent) = output_file_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
anyhow::anyhow!(
"failed to create directory (path: {}) for output: {}",
parent.display(),
e
)
})?;
}
let mut output_file = tokio::fs::File::create(&output_file_path)
.await
.map_err(|e| {
anyhow::anyhow!(
"failed to create file: {}, error: {}",
output_file_path.display(),
e
)
})?;
output_file.write_all(output.as_bytes()).await?;
} }
} }
} }
}
Commands::Serve { tracing::debug!("run is called, building extra commands, rerunning the parser");
host, let mut run_cmd = clap::Command::new("run").subcommand_required(true);
s3_endpoint,
s3_bucket, // 3. Provide context and aggregated commands for projects
s3_region, for (_, context) in &member_contexts {
s3_user, let commands = run::Run::augment_workspace_command(context, &context.project.name);
s3_password, run_cmd = run_cmd.subcommands(commands);
} => { }
tracing::info!("Starting server");
let creds = Credentials::new(s3_user, s3_password); run_cmd =
let bucket = Bucket::new( run_cmd.subcommand(clap::Command::new("all").allow_external_subcommands(true));
url::Url::parse(&s3_endpoint)?,
rusty_s3::UrlStyle::Path, let mut root = get_root(false).subcommand(run_cmd);
s3_bucket, let matches = root.get_matches_mut();
s3_region,
)?; if matches.subcommand().is_none() {
let put_object = bucket.put_object(Some(&creds), "some-object"); root.print_help()?;
let _url = put_object.sign(std::time::Duration::from_secs(30)); anyhow::bail!("failed to find command");
let _state = SharedState::new().await?; }
match matches
.subcommand()
.expect("forest requires a command to be passed")
{
("run", args) => {
let (run_args, args) = args.subcommand().expect("run must have subcommands");
match run_args {
"all" => {
let (all_cmd, _args) = args
.subcommand()
.expect("to be able to get a subcommand (todo: might not work)");
for (member_path, context) in member_contexts {
run::Run::execute_command_if_exists(all_cmd, member_path, &context)
.await?;
}
}
_ => {
let (project_name, command) = run_args
.split_once("::")
.expect("commands to always be pairs for workspaces");
let mut found_context = false;
for (member_path, context) in &member_contexts {
if project_name == context.project.name {
run::Run::execute_command(command, member_path, context)
.await?;
found_context = true;
}
}
if !found_context {
anyhow::bail!("no matching context was found")
}
}
}
}
_ => match Commands::from_arg_matches(&matches).unwrap() {
Commands::Init {} => {
tracing::info!("initializing project");
}
Commands::Info {} => {
let output = serde_json::to_string_pretty(&member_contexts)?;
println!("{}", output.to_colored_json_auto().unwrap_or(output));
}
Commands::Template(template) => {
//template.execute(&project_path, &context).await?;
}
Commands::Serve {
s3_endpoint,
s3_bucket,
s3_region,
s3_user,
s3_password,
..
} => {
tracing::info!("Starting server");
let creds = Credentials::new(s3_user, s3_password);
let bucket = Bucket::new(
url::Url::parse(&s3_endpoint)?,
rusty_s3::UrlStyle::Path,
s3_bucket,
s3_region,
)?;
let put_object = bucket.put_object(Some(&creds), "some-object");
let _url = put_object.sign(std::time::Duration::from_secs(30));
let _state = SharedState::new().await?;
}
Commands::Clean {} => {
todo!();
// let forest_path = project_path.join(".forest");
// if forest_path.exists() {
// tokio::fs::remove_dir_all(forest_path).await?;
// tracing::info!("removed .forest");
// }
}
},
}
}
ForestFile::Project(project) => {
tracing::trace!("found a project name: {}", project.name);
let plan = if let Some(plan_file_path) = PlanReconciler::new()
.reconcile(&project, &project_path, None, None)
.await?
{
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
let plan_doc: KdlDocument = plan_file.parse()?;
let plan: Plan = plan_doc.try_into()?;
tracing::trace!("found a plan name: {}", project.name);
Some(plan)
} else {
None
};
let context = Context { project, plan };
let matches = if matches.subcommand_matches("run").is_some() {
tracing::debug!("run is called, building extra commands, rerunning the parser");
let root = get_root(false);
let run_cmd = run::Run::augment_command(&context);
root.subcommand(run_cmd).get_matches()
} else {
matches
};
match matches
.subcommand()
.expect("forest requires a command to be passed")
{
("run", args) => {
run::Run::execute(args, &project_path, &context).await?;
}
_ => match Commands::from_arg_matches(&matches).unwrap() {
Commands::Init {} => {
tracing::info!("initializing project");
tracing::trace!("found context: {:?}", context);
}
Commands::Info {} => {
let output = serde_json::to_string_pretty(&context)?;
println!("{}", output.to_colored_json_auto().unwrap_or(output));
}
Commands::Template(template) => {
template.execute(&project_path, &context).await?;
}
Commands::Serve {
s3_endpoint,
s3_bucket,
s3_region,
s3_user,
s3_password,
..
} => {
tracing::info!("Starting server");
let creds = Credentials::new(s3_user, s3_password);
let bucket = Bucket::new(
url::Url::parse(&s3_endpoint)?,
rusty_s3::UrlStyle::Path,
s3_bucket,
s3_region,
)?;
let put_object = bucket.put_object(Some(&creds), "some-object");
let _url = put_object.sign(std::time::Duration::from_secs(30));
let _state = SharedState::new().await?;
}
Commands::Clean {} => {
let forest_path = project_path.join(".forest");
if forest_path.exists() {
tokio::fs::remove_dir_all(forest_path).await?;
tracing::info!("removed .forest");
}
}
},
}
} }
} }

View File

@@ -0,0 +1,166 @@
use std::path::Path;
use crate::{model::Context, script::ScriptExecutor};
// Run is a bit special in that because the arguments dynamically render, we need to do some special magic in
// clap to avoid having to do hacks to register clap subcommands midcommand. As such instead, we opt to simply
// create a new sub command that encapsulates all the run complexities
pub struct Run {}
impl Run {
pub fn augment_command(ctx: &Context) -> clap::Command {
let mut run_cmd = clap::Command::new("run")
.subcommand_required(true)
.about("runs any kind of script from either the project or plan");
if let Some(scripts) = &ctx.project.scripts {
for name in scripts.items.keys() {
let cmd = clap::Command::new(name.to_string());
run_cmd = run_cmd.subcommand(cmd);
}
}
if let Some(plan) = &ctx.plan {
if let Some(scripts) = &plan.scripts {
let existing_cmds = run_cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect::<Vec<_>>();
for name in scripts.items.keys() {
if existing_cmds.contains(name) {
continue;
}
let cmd = clap::Command::new(name.to_string());
run_cmd = run_cmd.subcommand(cmd);
}
}
}
run_cmd
}
pub fn augment_workspace_command(ctx: &Context, prefix: &str) -> Vec<clap::Command> {
let mut commands = Vec::new();
if let Some(scripts) = &ctx.project.scripts {
for name in scripts.items.keys() {
let cmd = clap::Command::new(format!("{prefix}::{name}"));
commands.push(cmd);
}
}
if let Some(plan) = &ctx.plan {
if let Some(scripts) = &plan.scripts {
let existing_cmds = commands
.iter()
.map(|s| format!("{prefix}::{}", s.get_name()))
.collect::<Vec<_>>();
for name in scripts.items.keys() {
if existing_cmds.contains(name) {
continue;
}
let cmd = clap::Command::new(format!("{prefix}::{name}"));
commands.push(cmd)
}
}
}
commands
}
pub async fn execute(
args: &clap::ArgMatches,
project_path: &Path,
ctx: &Context,
) -> anyhow::Result<()> {
let Some((name, args)) = args.subcommand() else {
anyhow::bail!("failed to find a matching run command")
};
if let Some(scripts_ctx) = &ctx.project.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(name) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, name)
.await?;
return Ok(());
}
}
if let Some(plan) = &ctx.plan {
if let Some(scripts_ctx) = &plan.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(name) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, name)
.await?;
return Ok(());
}
}
}
anyhow::bail!("no scripts were found for command: {}", name)
}
pub async fn execute_command(
command: &str,
project_path: &Path,
ctx: &Context,
) -> anyhow::Result<()> {
if let Some(scripts_ctx) = &ctx.project.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(command) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, command)
.await?;
return Ok(());
}
}
if let Some(plan) = &ctx.plan {
if let Some(scripts_ctx) = &plan.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(command) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, command)
.await?;
return Ok(());
}
}
}
anyhow::bail!("no scripts were found for command: {}", command)
}
pub async fn execute_command_if_exists(
command: &str,
project_path: &Path,
ctx: &Context,
) -> anyhow::Result<()> {
if let Some(scripts_ctx) = &ctx.project.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(command) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, command)
.await?;
return Ok(());
}
}
if let Some(plan) = &ctx.plan {
if let Some(scripts_ctx) = &plan.scripts {
if let Some(script_ctx) = scripts_ctx.items.get(command) {
ScriptExecutor::new(project_path.into(), ctx.clone())
.run(script_ctx, command)
.await?;
return Ok(());
}
}
}
Ok(())
}
}

View File

@@ -0,0 +1,206 @@
use std::path::Path;
use tokio::io::AsyncWriteExt;
use crate::model::{Context, TemplateType};
#[derive(clap::Parser)]
pub struct Template {}
impl Template {
pub async fn execute(self, project_path: &Path, context: &Context) -> anyhow::Result<()> {
tracing::info!("templating");
self.execute_plan(project_path, context).await?;
self.execute_project(project_path, context).await?;
Ok(())
}
async fn execute_plan(&self, project_path: &Path, context: &Context) -> anyhow::Result<()> {
let plan_path = &project_path.join(".forest").join("plan");
let Some(Some(template)) = &context.plan.as_ref().map(|p| &p.templates) else {
return Ok(());
};
match template.ty {
TemplateType::Jinja2 => {
for entry in glob::glob(&format!(
"{}/{}",
plan_path.display().to_string().trim_end_matches("/"),
template.path.trim_start_matches("./"),
))
.map_err(|e| anyhow::anyhow!("failed to read glob pattern: {}", e))?
{
let entry = entry.map_err(|e| anyhow::anyhow!("failed to read path: {}", e))?;
let entry_name = entry.display().to_string();
let entry_rel = if entry.is_absolute() {
entry.strip_prefix(plan_path).map(|e| e.to_path_buf())
} else {
Ok(entry.clone())
};
let rel_file_path = entry_rel
.map(|p| {
if p.file_name()
.map(|f| f.to_string_lossy().ends_with(".jinja2"))
.unwrap_or(false)
{
p.with_file_name(
p.file_stem().expect("to be able to find a filename"),
)
} else {
p.to_path_buf()
}
})
.map_err(|e| {
anyhow::anyhow!(
"failed to find relative file: {}, project: {}, file: {}",
e,
plan_path.display(),
entry_name
)
})?;
let output_file_path = project_path
.join(".forest/temp")
.join(&template.output)
.join(rel_file_path);
let contents = tokio::fs::read_to_string(&entry).await.map_err(|e| {
anyhow::anyhow!("failed to read template: {}, err: {}", entry.display(), e)
})?;
let mut env = minijinja::Environment::new();
env.add_template(&entry_name, &contents)?;
env.add_global("global", &context.project.global);
let tmpl = env.get_template(&entry_name)?;
let output = tmpl
.render(minijinja::context! {})
.map_err(|e| anyhow::anyhow!("failed to render template: {}", e))?;
tracing::info!("rendered template: {}", output);
if let Some(parent) = output_file_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
anyhow::anyhow!(
"failed to create directory (path: {}) for output: {}",
parent.display(),
e
)
})?;
}
let mut output_file = tokio::fs::File::create(&output_file_path)
.await
.map_err(|e| {
anyhow::anyhow!(
"failed to create file: {}, error: {}",
output_file_path.display(),
e
)
})?;
output_file.write_all(output.as_bytes()).await?;
}
}
}
Ok(())
}
async fn execute_project(&self, project_path: &Path, context: &Context) -> anyhow::Result<()> {
let Some(template) = &context.project.templates else {
return Ok(());
};
match template.ty {
TemplateType::Jinja2 => {
for entry in glob::glob(&format!(
"{}/{}",
project_path.display().to_string().trim_end_matches("/"),
template.path.trim_start_matches("./"),
))
.map_err(|e| anyhow::anyhow!("failed to read glob pattern: {}", e))?
{
let entry = entry.map_err(|e| anyhow::anyhow!("failed to read path: {}", e))?;
let entry_name = entry.display().to_string();
let entry_rel = if entry.is_absolute() {
entry.strip_prefix(project_path).map(|e| e.to_path_buf())
} else {
Ok(entry.clone())
};
let rel_file_path = entry_rel
.map(|p| {
if p.file_name()
.map(|f| f.to_string_lossy().ends_with(".jinja2"))
.unwrap_or(false)
{
p.with_file_name(
p.file_stem().expect("to be able to find a filename"),
)
} else {
p.to_path_buf()
}
})
.map_err(|e| {
anyhow::anyhow!(
"failed to find relative file: {}, project: {}, file: {}",
e,
project_path.display(),
entry_name
)
})?;
let output_file_path = project_path
.join(".forest/temp")
.join(&template.output)
.join(rel_file_path);
let contents = tokio::fs::read_to_string(&entry).await.map_err(|e| {
anyhow::anyhow!("failed to read template: {}, err: {}", entry.display(), e)
})?;
let mut env = minijinja::Environment::new();
env.add_template(&entry_name, &contents)?;
env.add_global("global", &context.project.global);
let tmpl = env.get_template(&entry_name)?;
let output = tmpl
.render(minijinja::context! {})
.map_err(|e| anyhow::anyhow!("failed to render template: {}", e))?;
tracing::info!("rendered template: {}", output);
if let Some(parent) = output_file_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
anyhow::anyhow!(
"failed to create directory (path: {}) for output: {}",
parent.display(),
e
)
})?;
}
let mut output_file = tokio::fs::File::create(&output_file_path)
.await
.map_err(|e| {
anyhow::anyhow!(
"failed to create file: {}, error: {}",
output_file_path.display(),
e
)
})?;
output_file.write_all(output.as_bytes()).await?;
}
}
}
Ok(())
}
}

View File

@@ -1,12 +1,23 @@
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
pub mod cli; pub mod cli;
pub mod model; pub mod model;
pub mod plan_reconciler; pub mod plan_reconciler;
pub mod script;
pub mod state; pub mod state;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
tracing_subscriber::fmt::init(); tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.with_env_var("FOREST_LOG_LEVEL")
.from_env()?,
)
.init();
cli::execute().await?; cli::execute().await?;

View File

@@ -1,6 +1,6 @@
use std::{collections::BTreeMap, fmt::Debug, path::PathBuf}; use std::{collections::BTreeMap, fmt::Debug, path::PathBuf};
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use kdl::{KdlDocument, KdlNode, KdlValue};
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -12,6 +12,10 @@ pub struct Context {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Plan { pub struct Plan {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Templates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<Scripts>,
} }
impl TryFrom<KdlDocument> for Plan { impl TryFrom<KdlDocument> for Plan {
@@ -35,6 +39,14 @@ impl TryFrom<KdlDocument> for Plan {
}) })
.cloned() .cloned()
.ok_or(anyhow::anyhow!("a forest kuddle plan must have a name"))?, .ok_or(anyhow::anyhow!("a forest kuddle plan must have a name"))?,
templates: plan_children
.get("templates")
.map(|t| t.try_into())
.transpose()?,
scripts: plan_children
.get("scripts")
.map(|m| m.try_into())
.transpose()?,
}) })
} }
} }
@@ -43,6 +55,8 @@ impl TryFrom<KdlDocument> for Plan {
#[serde(untagged)] #[serde(untagged)]
pub enum ProjectPlan { pub enum ProjectPlan {
Local { path: PathBuf }, Local { path: PathBuf },
Git { url: String, path: Option<PathBuf> },
Workspace { name: String },
NoPlan, NoPlan,
} }
@@ -64,6 +78,35 @@ impl TryFrom<&KdlNode> for ProjectPlan {
}); });
} }
if let Some(git) = children.get_arg("git") {
return Ok(Self::Git {
url: git
.as_string()
.map(|l| l.to_string())
.ok_or(anyhow::anyhow!("a git url is required"))?,
path: children
.get("git")
.and_then(|git| {
git.entries()
.iter()
.filter(|i| i.name().is_some())
.find(|i| i.name().expect("to have a value").to_string() == "path")
})
.and_then(|i| i.value().as_string().map(|p| p.to_string().into())),
});
}
if let Some(workspace) = children.get_arg("workspace") {
return Ok(Self::Workspace {
name: workspace
.as_string()
.map(|w| w.to_string())
.ok_or(anyhow::anyhow!(
"workspace requires a project name in the same project"
))?,
});
}
Ok(Self::NoPlan) Ok(Self::NoPlan)
} }
} }
@@ -130,9 +173,22 @@ impl TryFrom<&KdlValue> for GlobalVariable {
#[derive(Debug, Clone, Serialize, Default)] #[derive(Debug, Clone, Serialize, Default)]
pub struct Global { pub struct Global {
#[serde(flatten)]
items: BTreeMap<String, GlobalVariable>, items: BTreeMap<String, GlobalVariable>,
} }
impl Global {
fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl From<&Global> for minijinja::Value {
fn from(value: &Global) -> Self {
Self::from_serialize(&value.items)
}
}
impl TryFrom<&KdlNode> for Global { impl TryFrom<&KdlNode> for Global {
type Error = anyhow::Error; type Error = anyhow::Error;
@@ -197,7 +253,10 @@ impl TryFrom<&KdlNode> for Templates {
match val.to_lowercase().as_str() { match val.to_lowercase().as_str() {
"jinja2" => templates.ty = TemplateType::Jinja2, "jinja2" => templates.ty = TemplateType::Jinja2,
e => { e => {
anyhow::bail!("failed to find a template matching the required type: {}, only 'jinja2' is supported", e); anyhow::bail!(
"failed to find a template matching the required type: {}, only 'jinja2' is supported",
e
);
} }
} }
} }
@@ -224,12 +283,23 @@ impl TryFrom<&KdlNode> for Templates {
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Action {} #[serde(tag = "type")]
pub enum Script {
Shell {},
}
impl TryFrom<&KdlNode> for Script {
type Error = anyhow::Error;
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
Ok(Self::Shell {})
}
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Scripts { pub struct Scripts {
pub path: PathBuf, #[serde(flatten)]
pub actions: BTreeMap<String, Action>, pub items: BTreeMap<String, Script>,
} }
impl TryFrom<&KdlNode> for Scripts { impl TryFrom<&KdlNode> for Scripts {
@@ -237,22 +307,21 @@ impl TryFrom<&KdlNode> for Scripts {
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> { fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
let val = Self { let val = Self {
path: value items: {
.get("path") let mut out = BTreeMap::default();
.and_then(|p| p.as_string()) if let Some(children) = value.children() {
.map(PathBuf::from) for entry in children.nodes() {
.unwrap_or(PathBuf::from("scripts/")), let name = entry.name().value();
actions: value let val = entry.try_into()?;
.children()
.and_then(|c| c.get("actions")) out.insert(name.to_string(), val);
.and_then(|a| a.children()) }
.map(|d| {
d.nodes() out
.iter() } else {
.map(|n| (n.name().value().to_string(), Action {})) out
.collect::<BTreeMap<String, Action>>() }
}) },
.unwrap_or_default(),
}; };
Ok(val) Ok(val)
@@ -262,10 +331,16 @@ impl TryFrom<&KdlNode> for Scripts {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<ProjectPlan>, pub plan: Option<ProjectPlan>,
#[serde(skip_serializing_if = "Global::is_empty")]
pub global: Global, pub global: Global,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Templates>, pub templates: Option<Templates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<Scripts>, pub scripts: Option<Scripts>,
} }
@@ -322,3 +397,111 @@ impl TryFrom<KdlDocument> for Project {
}) })
} }
} }
#[derive(Debug, Clone, Serialize)]
pub struct WorkspaceMember {
pub path: String,
}
impl TryFrom<&kdl::KdlNode> for WorkspaceMember {
type Error = anyhow::Error;
fn try_from(value: &kdl::KdlNode) -> Result<Self, Self::Error> {
Ok(Self {
path: value
.entries()
.first()
.ok_or(anyhow::anyhow!(
"is supposed to have a path `member ./some-path`"
))?
.value()
.as_string()
.ok_or(anyhow::anyhow!("value is required to be a string"))?
.to_string(),
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Workspace {
pub members: Vec<WorkspaceMember>,
}
impl TryFrom<KdlDocument> for Workspace {
type Error = anyhow::Error;
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
let workspace = value
.get("workspace")
.expect("to have a workspace at this point")
.children()
.ok_or(anyhow::anyhow!("workspace to be a section"))?;
Ok(Self {
members: workspace
.get("members")
.ok_or(anyhow::anyhow!(
"a members section is required for a workspace"
))?
.children()
.ok_or(anyhow::anyhow!("a members is required to have children"))?
.nodes()
.iter()
.map(|m| m.try_into())
.collect::<anyhow::Result<Vec<_>>>()?,
})
}
}
#[derive(Debug, Clone, Serialize)]
pub enum ForestFile {
Workspace(Workspace),
Project(Project),
}
impl TryFrom<KdlDocument> for ForestFile {
type Error = anyhow::Error;
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
if value.get("workspace").is_some() && value.get("project").is_some() {
anyhow::bail!("a forest.kdl file cannot contain both a workspace and project")
}
if value.get("project").is_some() {
return Ok(Self::Project(value.try_into()?));
}
if value.get("workspace").is_some() {
return Ok(Self::Workspace(value.try_into()?));
}
anyhow::bail!("a forest.kdl file must be either a project, workspace or plan")
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum WorkspaceProject {
Plan(Plan),
Project(Project),
}
impl TryFrom<KdlDocument> for WorkspaceProject {
type Error = anyhow::Error;
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
if value.get("plan").is_some() && value.get("project").is_some() {
anyhow::bail!("a forest.kdl file cannot contain both a plan and project")
}
if value.get("project").is_some() {
return Ok(Self::Project(value.try_into()?));
}
if value.get("plan").is_some() {
return Ok(Self::Plan(value.try_into()?));
}
anyhow::bail!("a forest.kdl file must be either a project, workspace or plan")
}
}

View File

@@ -2,10 +2,13 @@ use std::path::{Path, PathBuf};
use anyhow::Context; use anyhow::Context;
use crate::model::Project; use crate::model::{Project, WorkspaceProject};
pub mod git;
pub mod local; pub mod local;
mod cache;
#[derive(Default)] #[derive(Default)]
pub struct PlanReconciler {} pub struct PlanReconciler {}
@@ -18,17 +21,25 @@ impl PlanReconciler {
&self, &self,
project: &Project, project: &Project,
destination: &Path, destination: &Path,
workspace_members: Option<&Vec<(PathBuf, WorkspaceProject)>>,
workspace_root: Option<&Path>,
) -> anyhow::Result<Option<PathBuf>> { ) -> anyhow::Result<Option<PathBuf>> {
tracing::info!("reconciling project"); tracing::info!("reconciling project");
if project.plan.is_none() { if project.plan.is_none() {
tracing::debug!("no plan, returning"); tracing::debug!("no plan, returning");
return Ok(None); return Ok(None);
} }
let cache = cache::Cache::new(destination);
// prepare the plan dir // prepare the plan dir
// TODO: We're always deleting, consider some form of caching // TODO: We're always deleting, consider some form of caching
let plan_dir = destination.join(".forest").join("plan"); let plan_dir = destination.join(".forest").join("plan");
if plan_dir.exists() { if plan_dir.exists() {
if let Some(secs) = cache.is_cache_valid().await? {
tracing::debug!("cache is valid for {} seconds", secs);
return Ok(Some(plan_dir.join("forest.kdl")));
}
tokio::fs::remove_dir_all(&plan_dir).await?; tokio::fs::remove_dir_all(&plan_dir).await?;
} }
tokio::fs::create_dir_all(&plan_dir) tokio::fs::create_dir_all(&plan_dir)
@@ -43,6 +54,24 @@ impl PlanReconciler {
let source = &destination.join(path); let source = &destination.join(path);
local::reconcile(source, &plan_dir).await?; local::reconcile(source, &plan_dir).await?;
} }
crate::model::ProjectPlan::Git { url, path } => {
git::reconcile(url, path, &plan_dir).await?;
}
crate::model::ProjectPlan::Workspace { name } => {
let workspace_root = workspace_root.expect("to have workspace root available");
if let Some(workspace_members) = workspace_members {
for (member_path, member) in workspace_members {
if let WorkspaceProject::Plan(plan) = member {
if &plan.name == name {
tracing::debug!("found workspace project: {}", name);
local::reconcile(&workspace_root.join(member_path), &plan_dir)
.await?;
}
}
}
}
}
crate::model::ProjectPlan::NoPlan => { crate::model::ProjectPlan::NoPlan => {
tracing::debug!("no plan, returning"); tracing::debug!("no plan, returning");
return Ok(None); return Ok(None);
@@ -51,6 +80,8 @@ impl PlanReconciler {
tracing::info!("reconciled project"); tracing::info!("reconciled project");
cache.upsert_cache().await?;
Ok(Some(plan_dir.join("forest.kdl"))) Ok(Some(plan_dir.join("forest.kdl")))
} }
} }

View File

@@ -0,0 +1,68 @@
use std::{
path::{Path, PathBuf},
time::UNIX_EPOCH,
};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
#[derive(Serialize, Deserialize, Clone, Debug)]
struct CacheFile {
last_update: u64,
}
pub struct Cache {
path: PathBuf,
}
impl Cache {
pub fn new(destination: &Path) -> Self {
Self {
path: destination.join(".forest").join("plan.cache.json"),
}
}
pub async fn is_cache_valid(&self) -> anyhow::Result<Option<u64>> {
if !self.path.exists() {
return Ok(None);
}
if let Ok(cache_config) = std::env::var("FOREST_CACHE").map(|c| c.trim().to_lowercase()) {
if cache_config.eq("no") || cache_config.eq("false") || cache_config.eq("0") {
return Ok(None);
}
}
let file = tokio::fs::read_to_string(&self.path).await?;
let cache_file: CacheFile = serde_json::from_str(&file)?;
let unix_cache = std::time::Duration::from_secs(cache_file.last_update);
let now = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?;
let cache_expire = now
.as_secs()
.saturating_sub(std::time::Duration::from_secs(60 * 60).as_secs()); // cache lasts an hour
if unix_cache.as_secs() > cache_expire {
return Ok(Some(unix_cache.as_secs().saturating_sub(cache_expire)));
}
Ok(None)
}
pub async fn upsert_cache(&self) -> anyhow::Result<()> {
if let Some(parent) = self.path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let unix = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?;
let cache_file = CacheFile {
last_update: unix.as_secs(),
};
let val = serde_json::to_string_pretty(&cache_file)?;
let mut file = tokio::fs::File::create(&self.path).await?;
file.write_all(val.as_bytes()).await?;
Ok(())
}
}

View File

@@ -0,0 +1,53 @@
use std::{
env::temp_dir,
path::{Path, PathBuf},
};
use super::local;
pub async fn reconcile(url: &str, path: &Option<PathBuf>, plan_dir: &Path) -> anyhow::Result<()> {
let temp = TempDir::new();
tokio::fs::create_dir_all(&temp.0).await?;
let mut cmd = tokio::process::Command::new("git");
cmd.args(["clone", url, &temp.0.display().to_string(), "--depth=1"]);
tracing::info!("cloning plan: {}", url);
let out = cmd.output().await?;
if !out.status.success() {
let stdout = std::str::from_utf8(&out.stdout).unwrap_or_default();
let stderr = std::str::from_utf8(&out.stderr).unwrap_or_default();
anyhow::bail!("failed to process git plan: {}, {}", stdout, stderr)
}
let temp_plan_dir = if let Some(path) = path {
temp.0.join(path)
} else {
temp.0.to_path_buf()
};
local::reconcile(&temp_plan_dir, plan_dir).await?;
drop(temp);
Ok(())
}
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
Self(
temp_dir()
.join("forest")
.join(uuid::Uuid::new_v4().to_string()),
)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).expect("to be able to remove temp dir");
}
}

View File

@@ -0,0 +1,61 @@
use std::path::PathBuf;
use shell::ShellExecutor;
use crate::model::{Context, Script};
pub mod shell;
#[derive(Clone)]
pub struct ScriptExecutor {
project_path: PathBuf,
ctx: Context,
}
impl ScriptExecutor {
pub fn new(project_path: PathBuf, ctx: Context) -> Self {
Self { project_path, ctx }
}
pub async fn run(&self, script_ctx: &Script, name: &str) -> anyhow::Result<()> {
if self.run_project(script_ctx, name).await? {
return Ok(());
}
if self.run_plan(script_ctx, name).await? {
return Ok(());
}
anyhow::bail!("script was not found for name: {}", name)
}
async fn run_project(&self, script_ctx: &Script, name: &str) -> anyhow::Result<bool> {
match script_ctx {
Script::Shell {} => {
if matches!(
ShellExecutor::from(self).execute(name).await?,
shell::ScriptStatus::Found
) {
Ok(true)
} else {
Ok(false)
}
}
}
}
async fn run_plan(&self, script_ctx: &Script, name: &str) -> anyhow::Result<bool> {
match script_ctx {
Script::Shell {} => {
if matches!(
ShellExecutor::from_plan(self).execute(name).await?,
shell::ScriptStatus::Found
) {
Ok(true)
} else {
Ok(false)
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
use std::{path::PathBuf, process::Stdio};
use anyhow::Context;
use super::ScriptExecutor;
pub struct ShellExecutor {
root: ScriptExecutor,
ty: ShellType,
}
pub enum ScriptStatus {
Found,
NotFound,
}
enum ShellType {
Plan,
Project,
}
impl ShellExecutor {
pub async fn execute(&self, name: &str) -> anyhow::Result<ScriptStatus> {
let path = &self.get_path();
let script_path = path.join("scripts").join(format!("{name}.sh"));
if !script_path.exists() {
return Ok(ScriptStatus::NotFound);
}
let mut cmd = tokio::process::Command::new(&script_path);
let cmd = cmd.current_dir(&self.root.project_path);
cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let mut proc = cmd.spawn().context(format!(
"failed to spawn process: {}",
script_path.display()
))?;
let exit = proc.wait().await?;
if !exit.success() {
anyhow::bail!(
"command: {name} failed with status: {}",
exit.code().unwrap_or(-1)
)
}
Ok(ScriptStatus::Found)
}
fn get_path(&self) -> PathBuf {
match self.ty {
//ShellType::Plan => self.root.project_path.join(".forest").join("plan"),
ShellType::Plan => self.root.project_path.join(".forest").join("plan"),
ShellType::Project => self.root.project_path.clone(),
}
}
pub fn from_plan(value: &ScriptExecutor) -> Self {
Self {
root: value.clone(),
ty: ShellType::Plan,
}
}
}
impl From<&ScriptExecutor> for ShellExecutor {
fn from(value: &ScriptExecutor) -> Self {
Self {
root: value.clone(),
ty: ShellType::Project,
}
}
}

View File

@@ -1,3 +1,13 @@
plan { plan {
name project name project
templates type=jinja2 {
path "templates/*.jinja2"
output "output/"
}
scripts {
world type=shell {}
hello type=shell {}
}
} }

5
examples/plan/scripts/hello.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
echo "hello plan"

5
examples/plan/scripts/world.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
echo "hello plan world"

View File

@@ -0,0 +1,3 @@
something plan
val is mapping: {{ global.someKey.some.key.val is mapping }}

View File

@@ -0,0 +1,3 @@
something plan
val is mapping: {{ global.someKey.some.key.val is mapping }}

View File

@@ -26,11 +26,8 @@ project {
output "output/" output "output/"
} }
scripts type=shell { scripts {
path "scripts/" hello type=shell {}
actions {
build
}
} }
} }

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
echo "hello world"

View File

@@ -1 +1,3 @@
something something
val is mapping: {{ global.someKey.some.key.val is mapping }}

View File

@@ -0,0 +1,33 @@
project {
name local
description """
A simple local project that depends on ../plan for its utility scripts
"""
plan {
git "ssh://git@git.front.kjuulh.io/kjuulh/forest" path="examples/plan"
}
global {
someName "name"
someKey {
someNestedKey "somevalue"
some {
key {
val
val
}
}
}
}
templates type=jinja2 {
path "templates/*.jinja2"
output "output/"
}
scripts {
hello type=shell {}
}
}

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
echo "hello world"

View File

@@ -0,0 +1,3 @@
something
val is mapping: {{ global.someKey.some.key.val is mapping }}

View File

@@ -0,0 +1,9 @@
workspace {
members {
member "projects/a"
member "projects/b"
member "plan/a"
member "plan/b"
// member "components/*"
}
}

View File

@@ -0,0 +1,7 @@
plan {
name a
scripts {
hello_plan type=shell {}
}
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env zsh
set -e
echo "hello from plan"
echo "i am here: $PWD"

View File

@@ -0,0 +1,3 @@
plan {
name b
}

View File

@@ -0,0 +1,11 @@
project {
name a
plan {
workspace a
}
scripts {
hello type=shell {}
}
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env zsh
set -e
echo "hello from a"
echo "i am here: $PWD"

View File

@@ -0,0 +1,7 @@
project {
name b
scripts {
hello type=shell {}
}
}

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -e
echo "hello from b"

View File

@@ -15,7 +15,7 @@
"type": "item", "type": "item",
"title": "should be able to download a remote plan", "title": "should be able to download a remote plan",
"description": "", "description": "",
"state": "not-done" "state": "done"
}, },
"should be able to template from a remote plan": { "should be able to template from a remote plan": {
"type": "item", "type": "item",
@@ -27,7 +27,7 @@
"type": "item", "type": "item",
"title": "should be able to use scripts from a remote plan", "title": "should be able to use scripts from a remote plan",
"description": "", "description": "",
"state": "not-done" "state": "done"
} }
} }
} }