Compare commits
23 Commits
257cb38cd5
...
main
Author | SHA1 | Date | |
---|---|---|---|
b9034de8a0 | |||
d0227cc9e5 | |||
f54b1b870a | |||
b3d6862195 | |||
045fc90364
|
|||
91fe491751
|
|||
e9e80abad0
|
|||
1fda414e05
|
|||
28a1d09974
|
|||
dca625af31
|
|||
bd927840d6 | |||
f98b48667c | |||
3510750c55
|
|||
0dd768af9d
|
|||
ffb81a5ee6
|
|||
dfe2ac62e3
|
|||
f10a1c0ac5
|
|||
b0261de87e
|
|||
2fadde0f8a
|
|||
9be64b74b2
|
|||
ef8deadfd8
|
|||
9b6996c261
|
|||
dcf459462e
|
2
.env
2
.env
@@ -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
|
||||||
|
41
.forest-ci/workflows/build.yaml
Normal file
41
.forest-ci/workflows/build.yaml
Normal 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
|
||||||
|
|
138
Cargo.lock
generated
138
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -69,9 +78,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.96"
|
version = "1.0.98"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -129,9 +138,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.31"
|
version = "4.5.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
|
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -139,9 +148,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.31"
|
version = "4.5.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
|
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -151,9 +160,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.28"
|
version = "4.5.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -491,15 +500,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.169"
|
version = "0.2.170"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||||
|
|
||||||
[[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 +522,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.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
@@ -558,18 +576,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minijinja"
|
name = "minijinja"
|
||||||
version = "2.7.0"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cff7b8df5e85e30b87c2b0b3f58ba3a87b68e133738bf512a7713769326dbca9"
|
checksum = "98642a6dfca91122779a307b77cd07a4aa951fbe32232aaf5bad9febc66be754"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
]
|
]
|
||||||
@@ -766,13 +784,57 @@ 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]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.9",
|
||||||
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@@ -821,18 +883,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.217"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.217"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -841,9 +903,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.138"
|
version = "1.0.140"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1009,9 +1071,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.43.0"
|
version = "1.44.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1086,10 +1148,14 @@ version = "0.3.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
@@ -1102,9 +1168,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"
|
||||||
@@ -1143,9 +1209,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.14.0"
|
version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
@@ -1362,18 +1428,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",
|
||||||
|
@@ -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"] }
|
||||||
|
@@ -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.7"
|
||||||
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"
|
||||||
|
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
166
crates/forest/src/cli/run.rs
Normal file
166
crates/forest/src/cli/run.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
206
crates/forest/src/cli/template.rs
Normal file
206
crates/forest/src/cli/template.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@@ -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?;
|
||||||
|
|
||||||
|
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
68
crates/forest/src/plan_reconciler/cache.rs
Normal file
68
crates/forest/src/plan_reconciler/cache.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
53
crates/forest/src/plan_reconciler/git.rs
Normal file
53
crates/forest/src/plan_reconciler/git.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
61
crates/forest/src/script.rs
Normal file
61
crates/forest/src/script.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
crates/forest/src/script/shell.rs
Normal file
77
crates/forest/src/script/shell.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
5
examples/plan/scripts/hello.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello plan"
|
5
examples/plan/scripts/world.sh
Executable file
5
examples/plan/scripts/world.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello plan world"
|
3
examples/plan/templates/something.plan.yaml.jinja2
Normal file
3
examples/plan/templates/something.plan.yaml.jinja2
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
something plan
|
||||||
|
|
||||||
|
val is mapping: {{ global.someKey.some.key.val is mapping }}
|
3
examples/plan/templates/something.yaml.jinja2
Normal file
3
examples/plan/templates/something.yaml.jinja2
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
something plan
|
||||||
|
|
||||||
|
val is mapping: {{ global.someKey.some.key.val is mapping }}
|
@@ -26,11 +26,8 @@ project {
|
|||||||
output "output/"
|
output "output/"
|
||||||
}
|
}
|
||||||
|
|
||||||
scripts type=shell {
|
scripts {
|
||||||
path "scripts/"
|
hello type=shell {}
|
||||||
actions {
|
|
||||||
build
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
examples/project/scripts/hello.sh
Executable file
5
examples/project/scripts/hello.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello world"
|
@@ -1 +1,3 @@
|
|||||||
something
|
something
|
||||||
|
|
||||||
|
val is mapping: {{ global.someKey.some.key.val is mapping }}
|
||||||
|
33
examples/project_git/forest.kdl
Normal file
33
examples/project_git/forest.kdl
Normal 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
5
examples/project_git/scripts/hello.sh
Executable file
5
examples/project_git/scripts/hello.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello world"
|
3
examples/project_git/templates/something.yaml.jinja2
Normal file
3
examples/project_git/templates/something.yaml.jinja2
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
something
|
||||||
|
|
||||||
|
val is mapping: {{ global.someKey.some.key.val is mapping }}
|
0
examples/workspace/components/flux/forest.kdl
Normal file
0
examples/workspace/components/flux/forest.kdl
Normal file
9
examples/workspace/forest.kdl
Normal file
9
examples/workspace/forest.kdl
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
workspace {
|
||||||
|
members {
|
||||||
|
member "projects/a"
|
||||||
|
member "projects/b"
|
||||||
|
member "plan/a"
|
||||||
|
member "plan/b"
|
||||||
|
// member "components/*"
|
||||||
|
}
|
||||||
|
}
|
7
examples/workspace/plan/a/forest.kdl
Normal file
7
examples/workspace/plan/a/forest.kdl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
plan {
|
||||||
|
name a
|
||||||
|
|
||||||
|
scripts {
|
||||||
|
hello_plan type=shell {}
|
||||||
|
}
|
||||||
|
}
|
7
examples/workspace/plan/a/scripts/hello_plan.sh
Executable file
7
examples/workspace/plan/a/scripts/hello_plan.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello from plan"
|
||||||
|
|
||||||
|
echo "i am here: $PWD"
|
3
examples/workspace/plan/b/forest.kdl
Normal file
3
examples/workspace/plan/b/forest.kdl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
plan {
|
||||||
|
name b
|
||||||
|
}
|
11
examples/workspace/projects/a/forest.kdl
Normal file
11
examples/workspace/projects/a/forest.kdl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
project {
|
||||||
|
name a
|
||||||
|
|
||||||
|
plan {
|
||||||
|
workspace a
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts {
|
||||||
|
hello type=shell {}
|
||||||
|
}
|
||||||
|
}
|
7
examples/workspace/projects/a/scripts/hello.sh
Executable file
7
examples/workspace/projects/a/scripts/hello.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello from a"
|
||||||
|
|
||||||
|
echo "i am here: $PWD"
|
7
examples/workspace/projects/b/forest.kdl
Normal file
7
examples/workspace/projects/b/forest.kdl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
project {
|
||||||
|
name b
|
||||||
|
|
||||||
|
scripts {
|
||||||
|
hello type=shell {}
|
||||||
|
}
|
||||||
|
}
|
5
examples/workspace/projects/b/scripts/hello.sh
Executable file
5
examples/workspace/projects/b/scripts/hello.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello from b"
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user