diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f97663b --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Database connection (same as Rails - both apps share the database) +DATABASE_URL=postgres://localhost/buildlight_development + +# Server port (use a different port than Rails if running both) +PORT=3001 + +# Host for generating URLs in webhooks +HOST=localhost:3001 + +# Particle IoT access token (optional) +PARTICLE_ACCESS_TOKEN= diff --git a/.gitignore b/.gitignore index bd63473..6858f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,21 +3,5 @@ # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile ~/.gitignore_global - -# Ignore bundler config -/.bundle - -# Ignore the default SQLite database. -/db/*.sqlite3 - -# Ignore all logfiles and tempfiles. -/log/*.log -/tmp - -# Ignore application configuration -/config/application.yml - -/app/assets/builds/* -!/app/assets/builds/.keep - -/node_modules +/target +.env diff --git a/.rspec b/.rspec deleted file mode 100644 index 83e16f8..0000000 --- a/.rspec +++ /dev/null @@ -1,2 +0,0 @@ ---color ---require spec_helper diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index f989260..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.4.4 diff --git a/.standard.yml b/.standard.yml deleted file mode 100644 index 2d3d5c4..0000000 --- a/.standard.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - - standard-performance - - standard-rails diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6422ecd..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: ruby -rvm: - - 3.3.1 -sudo: false -addons: - postgresql: '9.4' -branches: - only: - - master -before_script: - - bin/setup - - bundle exec rake db:migrate -notifications: - webhooks: - urls: - - https://buildlight.collectiveidea.com/ - on_start: always -cache: bundler diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a0d913d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3457 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "buildlight" +version = "0.1.0" +dependencies = [ + "async-stream", + "axum", + "chrono", + "dotenvy", + "futures", + "http-body-util", + "reqwest", + "serde", + "serde_json", + "sqlx", + "tera", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..92446dc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "buildlight" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8", features = ["ws"] } +tokio = { version = "1", features = ["full"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tera = "1" +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +reqwest = { version = "0.12", features = ["json"] } +tower-http = { version = "0.6", features = ["fs", "cors", "compression-gzip"] } +tower = "0.5" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +dotenvy = "0.15" +futures = "0.3" +async-stream = "0.3" + +[dev-dependencies] +http-body-util = "0.1" +tower = { version = "0.5", features = ["util"] } diff --git a/Dockerfile b/Dockerfile index 6392aef..685be74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,96 +1,55 @@ # syntax=docker/dockerfile:1 -# check=error=true -# Make sure RUBY_VERSION matches the Ruby version in .ruby-version -ARG RUBY_VERSION=3.4.4 -FROM ruby:$RUBY_VERSION-alpine AS base +# Build stage +FROM rust:1 AS build -LABEL fly_launch_runtime="rails" +WORKDIR /app -# Rails app lives here -WORKDIR /rails +# Install sqlx-cli for migrations +RUN cargo install sqlx-cli --no-default-features --features postgres -# Update gems and bundler -RUN gem update --system --no-document && \ - gem install -N bundler +# Copy manifests first for dependency caching +COPY Cargo.toml Cargo.lock ./ -# Install base packages -RUN apk add --no-cache curl jemalloc postgresql-client tzdata +# Create a dummy main to build dependencies +RUN mkdir src && echo "fn main() {}" > src/main.rs && echo "" > src/lib.rs +RUN cargo build --release && rm -rf src -# Set production environment -ENV BUNDLE_DEPLOYMENT="1" \ - BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development:test" \ - RAILS_ENV="production" +# Copy the actual source code and assets +COPY src ./src +COPY migrations ./migrations +COPY templates ./templates +COPY public ./public +COPY tests ./tests +# Touch main.rs so cargo rebuilds with actual code +RUN touch src/main.rs src/lib.rs +RUN cargo build --release -# Throw-away build stages to reduce size of final image -FROM base AS prebuild +# Runtime stage +FROM debian:stable-slim -# Install packages needed to build gems and node modules -RUN apk add --no-cache build-base git gyp libpq-dev pkgconfig python3 yaml-dev +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libpq5 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app -FROM prebuild AS node +# Copy the binary +COPY --from=build /app/target/release/buildlight ./bin/buildlight -# Install Node.js -ARG NODE_VERSION=22.4.0 -ENV PATH=/usr/local/node/bin:$PATH -RUN curl -sL https://unofficial-builds.nodejs.org/download/release/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64-musl.tar.gz | tar xz -C /tmp/ && \ - mkdir /usr/local/node && \ - cp -rp /tmp/node-v${NODE_VERSION}-linux-x64-musl/* /usr/local/node/ && \ - rm -rf /tmp/node-v${NODE_VERSION}-linux-x64-musl +# Copy sqlx-cli for running migrations manually if needed +COPY --from=build /usr/local/cargo/bin/sqlx ./bin/sqlx -# Install node modules -COPY package.json ./ -RUN npm install +# Copy runtime assets +COPY --from=build /app/migrations ./migrations +COPY --from=build /app/templates ./templates +COPY --from=build /app/public ./public - -FROM prebuild AS build - -# Install application gems -COPY Gemfile Gemfile.lock .ruby-version ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile - -# Copy node modules -COPY --from=node /rails/node_modules /rails/node_modules -COPY --from=node /usr/local/node /usr/local/node -ENV PATH=/usr/local/node/bin:$PATH - -# Copy application code -COPY . . - -# Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ - -# Adjust binfiles to set current working directory -RUN grep -l '#!/usr/bin/env ruby' /rails/bin/* | xargs sed -i '/^#!/aDir.chdir File.expand_path("..", __dir__)' - -# Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile - - -# Final stage for app image -FROM base - -# Install packages needed for deployment -RUN apk add --no-cache gzip libpq - -# Copy built artifacts: gems, application -COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" -COPY --from=build /rails /rails - -# Run and own only the runtime files as a non-root user for security -RUN addgroup --system --gid 1000 rails && \ - adduser --system rails --uid 1000 --ingroup rails --home /home/rails --shell /bin/sh rails && \ - chown -R 1000:1000 db log tmp +# Run as non-root +RUN groupadd --system --gid 1000 app && \ + useradd --system --uid 1000 --gid app app USER 1000:1000 -# Entrypoint sets up the container. -ENTRYPOINT ["/rails/bin/docker-entrypoint"] - -# Start the server by default, this can be overwritten at runtime -EXPOSE 3000 -CMD ["./bin/rails", "server"] +EXPOSE 8080 +CMD ["./bin/buildlight"] diff --git a/Gemfile b/Gemfile deleted file mode 100644 index f7abd31..0000000 --- a/Gemfile +++ /dev/null @@ -1,32 +0,0 @@ -source "https://rubygems.org" - -ruby file: ".ruby-version" - -gem "rails", "~> 8.0.1" - -gem "pg" - -gem "bootsnap" -gem "dockerfile-rails" -gem "honeybadger" -gem "ostruct" # Required by particlerb -gem "particlerb" -gem "puma" - -gem "importmap-rails" -gem "cssbundling-rails" -gem "propshaft" - -group :development, :test do - gem "debug" - gem "factory_bot_rails" - gem "figaro" - gem "rspec-rails" - gem "standard" - gem "standard-performance" - gem "standard-rails" -end - -group :test do - gem "rspec-ontap", require: false -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index d57b255..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,500 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) - mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) - mail (>= 2.8.0) - rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (8.0.2) - activesupport (= 8.0.2) - globalid (>= 0.3.6) - activemodel (8.0.2) - activesupport (= 8.0.2) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) - timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) - marcel (~> 1.0) - activesupport (8.0.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - ansi (1.5.0) - ast (2.4.3) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) - bootsnap (1.18.6) - msgpack (~> 1.2) - builder (3.3.0) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) - crass (1.0.6) - cssbundling-rails (1.4.3) - railties (>= 6.0.0) - date (3.4.1) - debug (1.10.0) - irb (~> 1.10) - reline (>= 0.3.8) - diff-lcs (1.6.2) - dockerfile-rails (1.7.9) - rails (>= 3.0.0) - drb (2.2.3) - erb (5.0.1) - erubi (1.13.1) - factory_bot (6.5.1) - activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) - factory_bot (~> 6.5) - railties (>= 5.0.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) - multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - figaro (1.2.0) - thor (>= 0.14.0, < 2) - globalid (1.2.1) - activesupport (>= 6.1) - honeybadger (5.28.0) - logger - ostruct - i18n (1.14.7) - concurrent-ruby (~> 1.0) - importmap-rails (2.1.0) - actionpack (>= 6.0.0) - activesupport (>= 6.0.0) - railties (>= 6.0.0) - io-console (0.8.0) - irb (1.15.2) - pp (>= 0.6.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - json (2.12.1) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - loofah (2.24.1) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.4) - mini_mime (1.1.5) - mini_portile2 (2.8.9) - minitest (5.25.5) - msgpack (1.8.0) - multipart-post (2.4.1) - net-imap (0.5.8) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.1) - net-protocol - nio4r (2.7.4) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-musl) - racc (~> 1.4) - ostruct (0.6.1) - parallel (1.27.0) - parser (3.3.8.0) - ast (~> 2.4.1) - racc - particlerb (2.1.0) - faraday (>= 0.9.0) - faraday_middleware (>= 0.9.0) - pg (1.5.9) - pp (0.6.2) - prettyprint - prettyprint (0.2.0) - prism (1.4.0) - propshaft (1.1.0) - actionpack (>= 7.0.0) - activesupport (>= 7.0.0) - rack - railties (>= 7.0.0) - psych (5.2.6) - date - stringio - puma (6.6.0) - nio4r (~> 2.0) - racc (1.8.1) - rack (3.1.15) - rack-session (2.1.1) - base64 (>= 0.1.0) - rack (>= 3.0.0) - rack-test (2.2.0) - rack (>= 1.3) - rackup (2.2.1) - rack (>= 3) - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) - bundler (>= 1.15.0) - railties (= 8.0.2) - rails-dom-testing (2.3.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) - irb (~> 1.13) - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.2.1) - rdoc (6.14.0) - erb - psych (>= 4.0.0) - regexp_parser (2.10.0) - reline (0.6.1) - io-console (~> 0.5) - rspec (3.13.0) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.4) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.4) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-ontap (0.3.1) - rspec - tapout - rspec-rails (8.0.0) - actionpack (>= 7.2) - activesupport (>= 7.2) - railties (>= 7.2) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) - rspec-support (3.13.3) - rubocop (1.75.7) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.44.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-performance (1.25.0) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.31.0) - activesupport (>= 4.2.0) - lint_roller (~> 1.1) - rack (>= 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - securerandom (0.4.1) - standard (1.50.0) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.75.5) - standard-custom (~> 1.0.0) - standard-performance (~> 1.8) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.8.0) - lint_roller (~> 1.1) - rubocop-performance (~> 1.25.0) - standard-rails (1.4.0) - lint_roller (~> 1.0) - rubocop-rails (~> 2.31.0) - stringio (3.1.7) - tapout (0.4.5) - ansi - json - thor (1.3.2) - timeout (0.4.3) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) - uri (1.0.3) - useragent (0.16.11) - websocket-driver (0.7.7) - base64 - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.7.3) - -PLATFORMS - aarch64-linux - arm64-darwin - ruby - x86_64-linux - x86_64-linux-musl - -DEPENDENCIES - bootsnap - cssbundling-rails - debug - dockerfile-rails - factory_bot_rails - figaro - honeybadger - importmap-rails - ostruct - particlerb - pg - propshaft - puma - rails (~> 8.0.1) - rspec-ontap - rspec-rails - standard - standard-performance - standard-rails - -CHECKSUMS - actioncable (8.0.2) sha256=7bcce2df62e91a80143592600e16583c273e98aab50ae40a9f6a2604bb3289a0 - actionmailbox (8.0.2) sha256=3d8fb3453913e6257da4d02004bbfa2b997dfd10672f8d990e95013983e2cedb - actionmailer (8.0.2) sha256=b0c968b38576ec56a3dc2795931818e0aaae6a18cc9801f53f175c12d4b277d0 - actionpack (8.0.2) sha256=93e703064f3815295ccf820f57acbca719aec836749597da9262781c9b2f4b78 - actiontext (8.0.2) sha256=a40db32032ee2fbb479d5d69318c4284344c1cda73836fd73ffcdb917d203abf - actionview (8.0.2) sha256=e038e1405cdfc18f04f17243da4fb8eeda3a4992f63a6d70a7281d255cf7cebb - activejob (8.0.2) sha256=b0228b45e36b1ef3a081c684e81494147e094a6baf729018756ccf125b1853ca - activemodel (8.0.2) sha256=0ae1fb7fa1fae0699ba041a9e97702df42ea3b13f2d39f2d0fde51fca5f0656c - activerecord (8.0.2) sha256=793470b92c44e4198d0262ac60086b7822f0ea585079ad67e32a6e4c86f2d90a - activestorage (8.0.2) sha256=f83d221e0f06ae38f2200e55490bd155c76d0add330f6e300e8646048d672977 - activesupport (8.0.2) sha256=8565cddba31b900cdc17682fd66ecd020441e3eef320a9930285394e8c07a45e - ansi (1.5.0) sha256=5408253274e33d9d27d4a98c46d2998266fd51cba58a7eb9d08f50e57ed23592 - ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 - base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507 - benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a - bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc - bootsnap (1.18.6) sha256=0ae2393c1e911e38be0f24e9173e7be570c3650128251bf06240046f84a07d00 - builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f - concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6 - connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b - crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d - cssbundling-rails (1.4.3) sha256=53aecd5a7d24ac9c8fcd92975acd0e830fead4ee4583d3d3d49bb64651946e41 - date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f - debug (1.10.0) sha256=11e28ca74875979e612444104f3972bd5ffb9e79179907d7ad46dba44bd2e7a4 - diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 - dockerfile-rails (1.7.9) sha256=a27d2f8c772ca2cd8cf8f3fcaea09a2a00c5bb1eaaca125f92e86cab3f5e35f8 - drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 - erb (5.0.1) sha256=760439803b36cc93eca8a266aab614614e588024a89bc30a62e78d98ff452c23 - erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 - factory_bot (6.5.1) sha256=40581ea7bec0aee05514b8f4f99ed477274bdf1884c1372de5209e60322d6ca9 - factory_bot_rails (6.4.4) sha256=139e17caa2c50f098fddf5e5e1f29e8067352024e91ca1186d018b36589e5c88 - faraday (1.10.4) sha256=a384c541cde688d68bf85055723aecb4100c3fa41b53beb2011b245960ab2f19 - faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 - faraday-em_synchrony (1.0.0) sha256=460dad1c30cc692d6e77d4c391ccadb4eca4854b315632cd7e560f74275cf9ed - faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 - faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b - faraday-multipart (1.1.0) sha256=856b0f1c7316a4d6c052dd2eef5c42f887d56d93a171fe8880da1af064ca0751 - faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 - faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 - faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 - faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 - faraday-retry (1.0.3) sha256=add154f4f399243cbe070806ed41b96906942e7f5259bb1fe6daf2ec8f497194 - faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 - figaro (1.2.0) sha256=5c035fec76e597226e591ce4501aba37e059013d87ab47a1de5ab3cd0649e2fa - globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9 - honeybadger (5.28.0) sha256=010acb869e49ea18796a2a3293aa99103802fdca3a94181db9a0163f1ead6431 - i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f - importmap-rails (2.1.0) sha256=9f10c67d60651a547579f448100d033df311c5d5db578301374aeb774faae741 - io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2 - irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba - json (2.12.1) sha256=fc8fe305c17c09c8ce6bc825f9aa61bcf232d835669b10472dffa862796f203b - language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc - lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 - logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.24.1) sha256=655a30842b70ec476410b347ab1cd2a5b92da46a19044357bbd9f401b009a337 - mail (2.8.1) sha256=ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad - marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4 - mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef - mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 - minitest (5.25.5) sha256=391b6c6cb43a4802bfb7c93af1ebe2ac66a210293f4a3fb7db36f2fc7dc2c756 - msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 - multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 - net-imap (0.5.8) sha256=52aa5fdfc1a8a3df1f793b20a327e95b5a9dfe1d733e1f0d53075d2dbcfcf593 - net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 - net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 - net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 - nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9 - nokogiri (1.18.8) sha256=8c7464875d9ca7f71080c24c0db7bcaa3940e8be3c6fc4bcebccf8b9a0016365 - nokogiri (1.18.8-aarch64-linux-gnu) sha256=36badd2eb281fca6214a5188e24a34399b15d89730639a068d12931e2adc210e - nokogiri (1.18.8-arm64-darwin) sha256=483b5b9fb33653f6f05cbe00d09ea315f268f0e707cfc809aa39b62993008212 - nokogiri (1.18.8-x86_64-linux-gnu) sha256=4a747875db873d18a2985ee2c320a6070c4a414ad629da625fbc58d1a20e5ecc - nokogiri (1.18.8-x86_64-linux-musl) sha256=ddd735fba49475a395b9ea793bb6474e3a3125b89960339604d08a5397de1165 - ostruct (0.6.1) sha256=09a3fb7ecc1fa4039f25418cc05ae9c82bd520472c5c6a6f515f03e4988cb817 - parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 - parser (3.3.8.0) sha256=2476364142b307fa5a1b1ece44f260728be23858a9c71078e956131a75453c45 - particlerb (2.1.0) sha256=3a644d14863e0c0cccb89bd5cacea60117ca6fdc058a7c01c7ba9bfa3bdd6859 - pg (1.5.9) sha256=761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc - pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff - prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 - prism (1.4.0) sha256=dc0e3e00e93160213dc2a65519d9002a4a1e7b962db57d444cf1a71565bb703e - propshaft (1.1.0) sha256=d389361faf66aeb17e8d204828962c1e506edd14a1a17adb3fa475435c070f6b - psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e - puma (6.6.0) sha256=f25c06873eb3d5de5f0a4ebc783acc81a4ccfe580c760cfe323497798018ad87 - racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f - rack (3.1.15) sha256=d12b3e9960d18a26ded961250f2c0e3b375b49ff40dbe6786e9c3b160cbffca4 - rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 - rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 - rackup (2.2.1) sha256=f737191fd5c5b348b7f0a4412a3b86383f88c43e13b8217b63d4c8d90b9e798d - rails (8.0.2) sha256=fdfaa5a83ec0388e02864e88d515959caedc88053b5f701c4deb1652d8f164c6 - rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560 - railties (8.0.2) sha256=0d7c3f40c49ba74980f1bac1d4bb153a9331c5ee8a9631d89c7bf79db82e5cf9 - rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a - rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d - rdoc (6.14.0) sha256=2c46de58d7129b8743fcf6d76e3db971bdc914150e15ac06b386549bd82ed7db - regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61 - reline (0.6.1) sha256=1afcc9d7cb1029cdbe780d72f2f09251ce46d3780050f3ec39c3ccc6b60675fb - rspec (3.13.0) sha256=d490914ac1d5a5a64a0e1400c1d54ddd2a501324d703b8cfe83f458337bab993 - rspec-core (3.13.3) sha256=25136507f4f9cf2e8977a2851e64e438b4331646054e345998714108745cdfe4 - rspec-expectations (3.13.4) sha256=4e43459765dfee900b25aa1361e106ab0799895ede65fc57872069feb559ecd8 - rspec-mocks (3.13.4) sha256=6bb158a0719c53d522104ed34c0777b884b2c9dc775ce64eaa10207df02ab993 - rspec-ontap (0.3.1) sha256=4560ff3b87e10b42a95447efc50a33a1098dba4d70756a7ecebf102c64b77b48 - rspec-rails (8.0.0) sha256=977a508cd94d152db2068c6585470db5d0cd47eef56d5410b9531034fb9d97bf - rspec-support (3.13.3) sha256=2a61e393f6e18b7228726e0c6869c5d5a1419d37206116c4d917d145276b3f43 - rubocop (1.75.7) sha256=23566ebb25263f26020687f8abb8aec049f3e29b6a00bdf0aa9d1db16b558be9 - rubocop-ast (1.44.1) sha256=e3cc04203b2ef04f6d6cf5f85fe6d643f442b18cc3b23e3ada0ce5b6521b8e92 - rubocop-performance (1.25.0) sha256=6f7d03568a770054117a78d0a8e191cefeffb703b382871ca7743831b1a52ec1 - rubocop-rails (2.31.0) sha256=79476e1075299c3e60fc50549c7c32614f9ebaae719b899ed75785c6786c52bd - ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 - ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef - securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 - standard (1.50.0) sha256=b6c67f61fd6cedeec90ee338c6e913d9ccc4c467660ad1575da8aa6ba10f4aec - standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b - standard-performance (1.8.0) sha256=ed17b7d0e061b2a19a91dd434bef629439e2f32310f22f26acb451addc92b788 - standard-rails (1.4.0) sha256=444d6cb8a096a9249e617ad693b02a714927a6bebe79320dc839b840c857f4c9 - stringio (3.1.7) sha256=5b78b7cb242a315fb4fca61a8255d62ec438f58da2b90be66048546ade4507fa - tapout (0.4.5) sha256=abae2ca6a24c9834f0e610128269d2d3627d5308705830efb77afa5569a6d148 - thor (1.3.2) sha256=eef0293b9e24158ccad7ab383ae83534b7ad4ed99c09f96f1a6b036550abbeda - timeout (0.4.3) sha256=9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e - tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b - unicode-display_width (3.1.4) sha256=8caf2af1c0f2f07ec89ef9e18c7d88c2790e217c482bfc78aaa65eadd5415ac1 - unicode-emoji (4.0.4) sha256=2c2c4ef7f353e5809497126285a50b23056cc6e61b64433764a35eff6c36532a - uri (1.0.3) sha256=e9f2244608eea2f7bc357d954c65c910ce0399ca5e18a7a29207ac22d8767011 - useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 - websocket-driver (0.7.7) sha256=056d99f2cd545712cfb1291650fde7478e4f2661dc1db6a0fa3b966231a146b4 - websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 - zeitwerk (2.7.3) sha256=b2e86b4a9b57d26ba68a15230dcc7fe6f040f06831ce64417b0621ad96ba3e85 - -RUBY VERSION - ruby 3.4.4p34 - -BUNDLED WITH - 2.6.9 diff --git a/Procfile b/Procfile deleted file mode 100644 index 5691041..0000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec puma -t 1:32 -b tcp://0.0.0.0:$PORT -release: rake db:migrate diff --git a/Procfile.dev b/Procfile.dev deleted file mode 100644 index c086f6a..0000000 --- a/Procfile.dev +++ /dev/null @@ -1,2 +0,0 @@ -web: bin/rails server -p 3000 -css: npm run build:css -- --watch diff --git a/README.md b/README.md index 8eb3b6a..185a19a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,79 @@ # BuildLight -[![Github Actions](https://github.com/collectiveidea/buildlight/actions/workflows/ci.yml/badge.svg)](https://github.com/collectiveidea/buildlight/actions/workflows/ci.yml)[![Build Status](https://travis-ci.org/collectiveidea/buildlight.svg?branch=master)](https://travis-ci.org/collectiveidea/buildlight) [![CircleCI](https://circleci.com/gh/collectiveidea/buildlight.svg?style=shield)](https://circleci.com/gh/collectiveidea/buildlight) [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) +[![Github Actions](https://github.com/collectiveidea/buildlight/actions/workflows/ci.yml/badge.svg)](https://github.com/collectiveidea/buildlight/actions/workflows/ci.yml) Catches webhooks from build services (GitHub Actions, Travis CI, Circle CI, etc.) and provides data to power our office stoplight. ![Collective Idea stoplight](https://buildlight.collectiveidea.com/collectiveidea.gif) +## Setup + +### Prerequisites + +- Rust (see `Cargo.toml` for edition) +- PostgreSQL + +### Install sqlx-cli + +```bash +cargo install sqlx-cli --no-default-features --features postgres +``` + +### Database + +Create the database and run migrations: + +```bash +createdb buildlight_development +sqlx migrate run +``` + +Copy `.env.example` to `.env` and update `DATABASE_URL` if needed. + +### Run + +```bash +cargo run +``` + +### Test + +Tests use `sqlx::test` which automatically creates and destroys temporary databases. Set `DATABASE_URL` to any connectable PostgreSQL database: + +```bash +cargo test +``` + +## Migrations + +Migrations live in `migrations/` as SQL files managed by [SQLx](https://github.com/launchbadge/sqlx). + +### Create a new migration + +```bash +sqlx migrate add -r +``` + +This creates a pair of files: +- `migrations/_.up.sql` — applied when migrating forward +- `migrations/_.down.sql` — applied when rolling back + +Write your SQL in each file, then run: + +```bash +sqlx migrate run +``` + +### Rollback + +```bash +sqlx migrate revert +``` + +### Migrations in production + +Migrations run automatically on app startup. No manual step is needed during deploy. + ## Add Projects ### GitHub Actions @@ -47,7 +115,7 @@ Go to your project settings in Circle CI and add a new Webhook with `https://bui ## Viewing Status -The [main website](https://buildlight.collectiveidea.com/) shows the basic status for all projects. Adding a user/organization name to the url shows just those projects, for example: [https://buildlight.collectiveidea.com/collectiveidea](https://buildlight.collectiveidea.com/collectiveidea). +The [main website](https://buildlight.collectiveidea.com/) shows the basic status for all projects. Adding a user/organization name to the url shows just those projects, for example: [https://buildlight.collectiveidea.com/collectiveidea](https://buildlight.collectiveidea.com/collectiveidea). Devices (editable only manually for now) can aggregate multiple organizations & projects, and have their own URL. For example, our office's physical light (see gif above) aggregates [@collectiveidea](https://github.com/collectiveidea), [@deadmanssnitch](https://github.com/deadmanssnitch), and client projects. Its URL is: https://buildlight.collectiveidea.com/devices/collectiveidea-office diff --git a/Rakefile b/Rakefile deleted file mode 100755 index b523e53..0000000 --- a/Rakefile +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env rake -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path("../config/application", __FILE__) - -Buildlight::Application.load_tasks - -task default: [:standard, "css:build", :spec] diff --git a/app.json b/app.json deleted file mode 100644 index 9b37423..0000000 --- a/app.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "buildpacks": [ - { - "url": "https://github.com/gaffneyc/heroku-buildpack-jemalloc.git" - }, - { - "url": "heroku/metrics" - }, - { - "url": "heroku/nodejs" - }, - { - "url": "heroku/ruby" - } - ], - - "scripts": { - "test-setup": "bundle install --with test && bin/setup && npm install yarn && yarn install", - "test": "RAILS_ENV=test bundle exec rake standard css:build && bundle exec rspec -f RSpec::TapY | tapout tap" - }, - "addons":[ - "heroku-postgresql:hobby-dev" - ] -} \ No newline at end of file diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index 57b0e8e..0000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,3 +0,0 @@ -//= link_tree ../builds -//= link_tree ../../javascript .js -//= link_tree ../../../vendor/javascript .js diff --git a/app/assets/stylesheets/application.sass.scss b/app/assets/stylesheets/application.sass.scss deleted file mode 100644 index 90bde63..0000000 --- a/app/assets/stylesheets/application.sass.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Entry point for your Sass build -@charset "utf-8"; - -@import "base/base"; -@import "components/bulb"; -@import "components/light"; -@import "components/message"; diff --git a/app/assets/stylesheets/base/_base.scss b/app/assets/stylesheets/base/_base.scss deleted file mode 100644 index 13eec6e..0000000 --- a/app/assets/stylesheets/base/_base.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import "variables"; -@import "reset"; -@import "elements"; -@import "keyframes"; diff --git a/app/assets/stylesheets/base/_elements.scss b/app/assets/stylesheets/base/_elements.scss deleted file mode 100644 index 95e179b..0000000 --- a/app/assets/stylesheets/base/_elements.scss +++ /dev/null @@ -1,28 +0,0 @@ -// Elements -// ----------------------------------------------------------------------------- - -html { - font-family: $base-font-family; - font-weight: 300; - line-height: 1.4; -} - -body { - background-color: $base-background-color; - color: $base-text-color; - font-size: 1em; -} - -ol, -ul { - padding-left: 1rem; -} - -a { - color: currentColor; - - &:focus, - &:hover { - color: $strong-text-color; - } -} diff --git a/app/assets/stylesheets/base/_keyframes.scss b/app/assets/stylesheets/base/_keyframes.scss deleted file mode 100644 index dd6c0fc..0000000 --- a/app/assets/stylesheets/base/_keyframes.scss +++ /dev/null @@ -1,3 +0,0 @@ -@keyframes pulse { - 50% { transform: scale(.8); } -} diff --git a/app/assets/stylesheets/base/_reset.scss b/app/assets/stylesheets/base/_reset.scss deleted file mode 100644 index 2af04dd..0000000 --- a/app/assets/stylesheets/base/_reset.scss +++ /dev/null @@ -1,28 +0,0 @@ -// Reset (the bits of Normalize.css that are used in this site) -// ----------------------------------------------------------------------------- - -// Prevent adjustments of font size after orientation changes in -// IE on Windows Phone and in iOS. -html { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} - -// Remove the margin in all browsers (opinionated). -body { - margin: 0; -} - -// 1. Remove the gray background on active links in IE 10. -// 2. Remove gaps in links underline in iOS 8+ and Safari 8+. -a { - background-color: transparent; // 1 - -webkit-text-decoration-skip: objects; // 2 -} - -// Remove the outline on focused links when they are also active or hovered -// in all browsers (opinionated). -a:active, -a:hover { - outline-width: 0; -} diff --git a/app/assets/stylesheets/base/_variables.scss b/app/assets/stylesheets/base/_variables.scss deleted file mode 100644 index fec9162..0000000 --- a/app/assets/stylesheets/base/_variables.scss +++ /dev/null @@ -1,33 +0,0 @@ -// Variables -// ----------------------------------------------------------------------------- - -// Colors -$black: #000; -$white: #fff; -$red: #cf4c29; -$yellow: #ecc561; -$green: #28b56e; - -$base-background-color: mix($black, $white, 75%); - -$base-text-color: mix($black, $white, 10%); -$strong-text-color: $white; -$muted-text-color: mix($black, $white, 50%); - -// Typography -$base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - -// Transitions -$base-transition-speed: 500ms; -$fast-transition-speed: $base-transition-speed * 0.5; -$slow-transition-speed: $base-transition-speed * 2; -$slug-transition-speed: $base-transition-speed * 8; - -// Traffic light -$bulb-size: 50vmin; -$bulb-shadow-size: 150vmax; -$bulb-gutter: 6vmin; -$light-height: ($bulb-size * 3) + ($bulb-gutter * 2); - -// Animations -$animation-pulse-duration: 5s; diff --git a/app/assets/stylesheets/components/_bulb.scss b/app/assets/stylesheets/components/_bulb.scss deleted file mode 100644 index 458ea1c..0000000 --- a/app/assets/stylesheets/components/_bulb.scss +++ /dev/null @@ -1,91 +0,0 @@ -// Lightbulbs - the circles that sit within the traffic light -// ----------------------------------------------------------------------------- - -@use "sass:math"; - -.bulb { - height: $bulb-size; - position: relative; - width: $bulb-size; - - &__glow { - animation: pulse $animation-pulse-duration infinite; - display: block; - height: $bulb-shadow-size; - left: 50%; - margin-left: -($bulb-shadow-size * 0.5); - margin-top: -($bulb-shadow-size * 0.5); - opacity: 0; - position: absolute; - top: 50%; - transform: scale(.5); - transition: transform $slow-transition-speed, opacity $slow-transition-speed; - transition-delay: $fast-transition-speed; - width: $bulb-shadow-size; - z-index: 2; - - // 1 - Make sure the building glow displays underneath the - // passing/failing glows; they should take precedence - .bulb--yellow & { z-index: 1; } // 1 - } - - &__disc { - background-color: rgba($black, .2); - border-radius: 50%; - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - transition: background-color $fast-transition-speed; - z-index: 3; - } - - &__text { - font-size: math.div($bulb-size, 7); - font-weight: 800; - left: $bulb-size * 0.5; - line-height: 1; - margin-left: -($bulb-size * 0.5); - margin-top: -(math.div($bulb-size, 7) * 0.5); - opacity: 0; - position: absolute; - text-align: center; - text-transform: lowercase; - top: $bulb-size * 0.5; - transition: opacity $base-transition-speed; - width: $bulb-size; - z-index: 4; - } - - @mixin bulb($color, $state) { - [data-#{ $state }] & { - .bulb__glow { - background-image: radial-gradient( - ellipse at center, - rgba($color, .9) 10%, - rgba($color, .6) 20%, - rgba($color, .3) 40%, - rgba($color, 0) 70% - ); - opacity: 1; - transform: scale(1); - } - - .bulb__disc { background-color: $color; } - .bulb__text { opacity: 1; } - } - } - - &--red { @include bulb($red, "failing"); } - - &--yellow { - @include bulb($yellow, "building"); - - // 1 - offset glow animation so that the building light - // balances well with the failing/passing ones - .bulb__glow { animation-delay: -($animation-pulse-duration * 0.5); } // 1 - } - - &--green { @include bulb($green, "passing"); } -} diff --git a/app/assets/stylesheets/components/_light.scss b/app/assets/stylesheets/components/_light.scss deleted file mode 100644 index 64131ef..0000000 --- a/app/assets/stylesheets/components/_light.scss +++ /dev/null @@ -1,44 +0,0 @@ -// Traffic light - the box that holds the light bulbs -// ----------------------------------------------------------------------------- - -.light { - bottom: 0; - left: 0; - overflow: hidden; - position: absolute; - right: 0; - top: 0; - - &__box { - display: flex; - flex-direction: column; - height: $light-height; - justify-content: space-between; - left: calc(50vw - (#{ $bulb-size } / 2)); - position: absolute; - top: calc(50vh - (#{ $light-height } / 2)); - transition: margin-top $base-transition-speed; - width: $bulb-size; - - [data-failing] & { - margin-top: $bulb-size + $bulb-gutter; - } - - [data-failing][data-building] & { - margin-top: ($bulb-size + $bulb-gutter) * 0.5; - } - - [data-passing] & { - margin-top: -($bulb-size + $bulb-gutter); - } - - [data-passing][data-building] & { - margin-top: -($bulb-size + $bulb-gutter) * 0.5; - } - - [data-failing][data-passing] & { - margin-top: 0; - transform: scale(.75); - } - } -} diff --git a/app/assets/stylesheets/components/_message.scss b/app/assets/stylesheets/components/_message.scss deleted file mode 100644 index 7010d9d..0000000 --- a/app/assets/stylesheets/components/_message.scss +++ /dev/null @@ -1,46 +0,0 @@ -// Messages -// ----------------------------------------------------------------------------- - -.messages { - left: 5vmin; - position: absolute; - top: 5vmin; - z-index: 10; - - &__footer { - font-size: .8em; - opacity: .5; - - :first-child { margin-top: 0; } - } -} - -.message { - font-size: 0; - overflow: hidden; - padding-bottom: .5em; - transition: font-size $base-transition-speed, - opacity $base-transition-speed; - - .emoji { - height: 1em; - width: auto; - } - - > * { - margin-top: 0; - } - - > :first-child { - font-size: 1.5em; - line-height: 1.2; - margin-bottom: .25em; - } - - [data-failing] &--failure, - [data-building] &--building, - [data-passing] &--passing { - font-size: 1em; - opacity: 1; - } -} diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb deleted file mode 100644 index d672697..0000000 --- a/app/channels/application_cable/channel.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ApplicationCable - class Channel < ActionCable::Channel::Base - end -end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb deleted file mode 100644 index 0ff5442..0000000 --- a/app/channels/application_cable/connection.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ApplicationCable - class Connection < ActionCable::Connection::Base - end -end diff --git a/app/channels/colors_channel.rb b/app/channels/colors_channel.rb deleted file mode 100644 index 285a734..0000000 --- a/app/channels/colors_channel.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ColorsChannel < ApplicationCable::Channel - def subscribed - stream_from "colors:#{params[:id]}" - end - - def unsubscribed - # Any cleanup needed when channel is unsubscribed - end -end diff --git a/app/channels/device_channel.rb b/app/channels/device_channel.rb deleted file mode 100644 index 95b4b86..0000000 --- a/app/channels/device_channel.rb +++ /dev/null @@ -1,28 +0,0 @@ -class DeviceChannel < ApplicationCable::Channel - after_subscribe :initial_broadcast - - def subscribed - @slug = params[:id] - - # Uses friend device slug rather than id. - # device:my-slug - stream_from "device:#{@slug}" - end - - def unsubscribed - # Any cleanup needed when channel is unsubscribed - end - - def initial_broadcast - # Trigger an initial broadcast. - Thread.new do - device = Device.find_by(slug: @slug) - if device - 5.times do - device.broadcast - sleep 30 - end - end - end - end -end diff --git a/app/controllers/api/application_controller.rb b/app/controllers/api/application_controller.rb deleted file mode 100644 index 000da4b..0000000 --- a/app/controllers/api/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class API::ApplicationController < ActionController::API -end diff --git a/app/controllers/api/devices_controller.rb b/app/controllers/api/devices_controller.rb deleted file mode 100644 index 2b704b8..0000000 --- a/app/controllers/api/devices_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class API::DevicesController < API::ApplicationController - def show - device = Device.find(params[:id]) - render json: {colors: device.colors_as_booleans, ryg: device.ryg} - end - - def trigger - device = Device.find_by(identifier: params[:coreid]) - device&.trigger - head :ok - end -end diff --git a/app/controllers/api/red_controller.rb b/app/controllers/api/red_controller.rb deleted file mode 100644 index 8fedd3f..0000000 --- a/app/controllers/api/red_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class API::RedController < ApplicationController - def show - device = Device.find_sole_by(identifier: params[:id]) - @red_projects = device.statuses.where(red: true) - respond_to do |format| - format.html - format.json { render json: @red_projects.to_json(only: [:username, :project_name]) } - end - end -end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb deleted file mode 100644 index 09705d1..0000000 --- a/app/controllers/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationController < ActionController::Base -end diff --git a/app/controllers/colors_controller.rb b/app/controllers/colors_controller.rb deleted file mode 100644 index eb0a769..0000000 --- a/app/controllers/colors_controller.rb +++ /dev/null @@ -1,33 +0,0 @@ -class ColorsController < ApplicationController - include ActionController::Live - - def index - respond_to do |format| - format.html do - @colors = Status.colors(@ids) - render "index" - end - format.ryg do - response.headers["Content-Type"] = "text/ryg" - - ActiveRecord::Base.connection_pool.release_connection - - loop do - ActiveRecord::Base.connection_pool.with_connection do - response.stream.write(Status.uncached { Status.ryg(@ids) }) - end - - sleep 1 - end - rescue IOError - response.stream.close - end - format.json { render json: Status.colors(@ids) } - end - end - - def show - @ids = params[:id].split(",") - index - end -end diff --git a/app/controllers/devices_controller.rb b/app/controllers/devices_controller.rb deleted file mode 100644 index 602ed99..0000000 --- a/app/controllers/devices_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -class DevicesController < ApplicationController - def show - device = Device.where(slug: params[:id]).or(Device.where(id: params[:id])).sole - - respond_to do |format| - format.html do - @colors = device.colors - render "colors/index" - end - format.json { render json: device.colors } - end - end -end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb deleted file mode 100644 index 47897c7..0000000 --- a/app/controllers/webhooks_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "digest" -class WebhooksController < API::ApplicationController - def create - if params[:payload].is_a?(String) - ParseTravis.call(params[:payload]) - head :ok - elsif params[:payload].blank? && params[:repository]&.include?("/") - ParseGithub.call(params) - head :ok - elsif request.headers.env["HTTP_CIRCLECI_EVENT_TYPE"].present? - ParseCircle.call(params) - head :ok - else - head :bad_request - end - end -end diff --git a/app/helpers/color_helper.rb b/app/helpers/color_helper.rb deleted file mode 100644 index 19b6666..0000000 --- a/app/helpers/color_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ColorHelper - def color_attrs_for_body(colors) - html = +"" - - if colors - html << if colors[:red] - " data-failing" - else - " data-passing" - end - - html << " data-building" if colors[:yellow] - end - - html - end - - def color_favicon_link_tag(colors) - filename = +"/favicon" - - if colors - filename << if colors[:red] - "-failing" - else - "-passing" - end - - filename << "-building" if colors[:yellow] - end - - filename << ".ico" - favicon_link_tag filename, id: "favicon" - end -end diff --git a/app/interactors/parse_circle.rb b/app/interactors/parse_circle.rb deleted file mode 100644 index 3779e78..0000000 --- a/app/interactors/parse_circle.rb +++ /dev/null @@ -1,20 +0,0 @@ -class ParseCircle - def self.call(payload) - return unless payload["type"] == "workflow-completed" - # Ignore pull requests. We can't really determine a PR build, so we ignore any branches other than main/master. - return unless payload.dig("pipeline", "vcs", "branch").in? ["main", "master"] - - status = Status.find_or_initialize_by(service: "circle", username: payload["organization"]["name"], project_name: payload["project"]["name"]) - status.payload = payload if Rails.configuration.x.debug - set_colors(status, payload["workflow"]["status"]) - status.save! - end - - # Potential Statuses - # See: https://circleci.com/docs/webhooks/#event-specifications - # "success", "failed", "error", "canceled", "unauthorized" - def self.set_colors(status, code) - status.yellow = false # currently no way to set yellow on Circle - status.red = code != "success" - end -end diff --git a/app/interactors/parse_github.rb b/app/interactors/parse_github.rb deleted file mode 100644 index 7032950..0000000 --- a/app/interactors/parse_github.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ParseGithub - def self.call(payload) - username, project_name = payload["repository"].split("/") - workflow = payload["workflow"] - status = Status.find_or_initialize_by(service: "github", username: username, project_name: project_name, workflow: workflow) - status.payload = payload if Rails.configuration.x.debug - set_colors(status, payload["status"]) - status.save! - end - - # Options - # "success", "failure", "" - def self.set_colors(status, code) - status.yellow = false - case code - when "" - status.yellow = true - when "success" - status.red = false - when "failure" - status.red = true - else - raise "Unknown status: #{code}" - end - end -end diff --git a/app/interactors/parse_travis.rb b/app/interactors/parse_travis.rb deleted file mode 100644 index ebc6558..0000000 --- a/app/interactors/parse_travis.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ParseTravis - def self.call(payload) - json = JSON.parse(payload) - return if json["type"] == "pull_request" - - status = Status.find_or_initialize_by(service: "travis", project_id: json["repository"]["id"].to_s) - status.payload = payload if Rails.configuration.x.debug - status.username = json["repository"]["owner_name"] - status.project_name = json["repository"]["name"] - set_colors(status, json["status_message"]) - status.save! - end - - # Set colors based on travis-ci's status code - def self.set_colors(status, code) - status.yellow = false - case code - when "Pending" - status.yellow = true - when "Passed", "Fixed" - status.red = false - else - status.red = true - end - end -end diff --git a/app/interactors/trigger_particle.rb b/app/interactors/trigger_particle.rb deleted file mode 100644 index dcf5105..0000000 --- a/app/interactors/trigger_particle.rb +++ /dev/null @@ -1,5 +0,0 @@ -class TriggerParticle - def self.call(device) - Particle.publish(name: "build_state", data: device.status, ttl: 3600, private: false) - end -end diff --git a/app/interactors/trigger_webhook.rb b/app/interactors/trigger_webhook.rb deleted file mode 100644 index 6f43c9a..0000000 --- a/app/interactors/trigger_webhook.rb +++ /dev/null @@ -1,11 +0,0 @@ -class TriggerWebhook - def self.call(device) - Faraday.post(device.webhook_url, - {colors: device.colors_as_booleans}.to_json, - { - "Content-Type": "application/json", - "x-ryg": device.ryg, - "x-device-url": Rails.application.routes.url_helpers.api_device_url(device, host: Rails.configuration.x.host) - }) - end -end diff --git a/app/javascript/application.js b/app/javascript/application.js deleted file mode 100644 index c5b71a3..0000000 --- a/app/javascript/application.js +++ /dev/null @@ -1,3 +0,0 @@ -// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails - -import "channels" diff --git a/app/javascript/channels/colors_channel.js b/app/javascript/channels/colors_channel.js deleted file mode 100644 index d3ef3fb..0000000 --- a/app/javascript/channels/colors_channel.js +++ /dev/null @@ -1,55 +0,0 @@ -import consumer from "channels/consumer" - -let ids, channel; - -if (document.location.pathname.match(/^\/devices\//)) { - ids = document.location.pathname.match(/^\/devices\/([^\/\?]*)/)[1].split(","); - channel = "DeviceChannel"; -} else { - ids = document.location.pathname.match(/^\/([^\/\?]*)/)[1].split(","); - channel = "ColorsChannel"; -} - -ids.forEach(function (id) { - if (id === "") { - id = "*"; - } - - consumer.subscriptions.create({ channel: channel, id: id }, { - connected() { - // Called when the subscription is ready for use on the server - }, - - disconnected() { - // Called when the subscription has been terminated by the server - }, - - received(data) { - // Called when there's incoming data on the websocket for this channel - var count, favicon, message, redCount; - redCount = +data.colors.red; - favicon = redCount ? "/favicon-failing" : "/favicon-passing"; - if (redCount > 0) { - document.body.setAttribute("data-failing", ""); - document.body.removeAttribute("data-passing"); - count = document.getElementById("failing-count"); - if (redCount === 1) { - message = "" + redCount + " project is"; - } else { - message = "" + redCount + " projects are"; - } - count.innerHTML = message; - } else { - document.body.removeAttribute("data-failing"); - document.body.setAttribute("data-passing", ""); - } - if (data.colors.yellow) { - document.body.setAttribute("data-building", ""); - favicon += "-building"; - } else { - document.body.removeAttribute("data-building"); - } - return document.getElementById("favicon").setAttribute("href", favicon + ".ico"); - } - }); -}); diff --git a/app/javascript/channels/consumer.js b/app/javascript/channels/consumer.js deleted file mode 100644 index 8ec3aad..0000000 --- a/app/javascript/channels/consumer.js +++ /dev/null @@ -1,6 +0,0 @@ -// Action Cable provides the framework to deal with WebSockets in Rails. -// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. - -import { createConsumer } from "@rails/actioncable" - -export default createConsumer() diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js deleted file mode 100644 index 0c7cd18..0000000 --- a/app/javascript/channels/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// Import all the channels to be used by Action Cable -import "channels/colors_channel" diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/.gitkeep b/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/application_record.rb b/app/models/application_record.rb deleted file mode 100644 index 10a4cba..0000000 --- a/app/models/application_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true -end diff --git a/app/models/device.rb b/app/models/device.rb deleted file mode 100644 index eba3a0f..0000000 --- a/app/models/device.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Device < ApplicationRecord - validates :name, presence: true - validates :identifier, uniqueness: {allow_blank: true} - validates :slug, uniqueness: {allow_blank: true} - - # Ensure the status is updated if we change anything - after_commit :update_status, on: [:create, :update] - - def statuses - Status.where(username: usernames) - .or(Status.where("(username || '/' || project_name) IN (?)", projects)) - end - - def colors - statuses.colors - end - - def colors_as_booleans - statuses.colors_as_booleans - end - - def ryg - statuses.ryg - end - - def update_status - self.status = statuses.current_status - broadcast - - if status_changed? - self.status_changed_at = Time.current - save! - # Currenly only triggering on status change to reduce webhooks - trigger - end - end - - def broadcast - DeviceChannel.broadcast_to(slug, colors: colors) if slug - end - - def trigger - TriggerWebhook.call(self) if webhook_url - TriggerParticle.call(self) if identifier - end -end diff --git a/app/models/status.rb b/app/models/status.rb deleted file mode 100644 index a0a5124..0000000 --- a/app/models/status.rb +++ /dev/null @@ -1,47 +0,0 @@ -class Status < ApplicationRecord - after_commit :update_devices - - scope :red, -> { where(red: true) } - - def name - "#{username}/#{project_name}" - end - - # Devices that are "watching" this Status - def devices - Device.where("usernames @> ARRAY[?]::varchar[]", [username]) - .or(Device.where("projects @> ARRAY[?]::varchar[]", [name])) - end - - # Colors for all statuses - # Red may be a count not a boolean. Use colors_as_booleans for boolean values. - def self.colors(username = nil) - user_scope = username.present? ? where(username: username) : all - red = user_scope.where(red: true).count - red = false if red.zero? - yellow = user_scope.where(yellow: true).any? - - {red: red, yellow: yellow, green: !red} - end - - def self.colors_as_booleans(username = nil) - colors(username).transform_values { |v| !!v } - end - - def self.ryg(username = nil) - colors(username).map { |k, v| v ? k[0].upcase : k[0].downcase }.join - end - - def self.current_status - red = "failing" if where(red: true).any? - yellow = "building" if where(yellow: true).any? - green = "passing" unless red - [green, red, yellow].compact.join("-") # combines status to send "passing|failing-building" - end - - def update_devices - ColorsChannel.broadcast_to("*", colors: Status.colors) - ColorsChannel.broadcast_to(username, colors: Status.colors(username)) - devices.each(&:update_status) - end -end diff --git a/app/views/api/red/show.html.erb b/app/views/api/red/show.html.erb deleted file mode 100644 index 473d3cb..0000000 --- a/app/views/api/red/show.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -
- <% if @red_projects.any? %> -

The following projects are failing

-
    - <% @red_projects.each do |red| %> -
  • <%= red.project_name %>
  • - <% end %> -
- <% else %> -
- <%= "🎉" * 5 %> -
-

You have no failing projects.

-
- <%= "🎉" * 5 %> -
- <% end %> -
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb deleted file mode 100644 index 7a7b47d..0000000 --- a/app/views/layouts/application.html.erb +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - [i] Buildlight - <%= color_favicon_link_tag(@colors) %> - <%= csrf_meta_tags %> - <%= action_cable_meta_tag %> - <%= stylesheet_link_tag "application", :media => "all" %> - <%= javascript_importmap_tags %> - - - > - <%= yield %> - - diff --git a/bin/bundle b/bin/bundle deleted file mode 100755 index f19acf5..0000000 --- a/bin/bundle +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -load Gem.bin_path('bundler', 'bundle') diff --git a/bin/dev b/bin/dev deleted file mode 100755 index f709c1f..0000000 --- a/bin/dev +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -foreman start -f Procfile.dev diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint deleted file mode 100755 index c12e7ae..0000000 --- a/bin/docker-entrypoint +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -e - -# Enable jemalloc for reduced memory usage and latency. -if [ -z "${LD_PRELOAD+x}" ]; then - LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) - export LD_PRELOAD -fi - -# Add any container initialization steps here - -exec "${@}" diff --git a/bin/importmap b/bin/importmap deleted file mode 100755 index 36502ab..0000000 --- a/bin/importmap +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/application" -require "importmap/commands" diff --git a/bin/rails b/bin/rails deleted file mode 100755 index efc0377..0000000 --- a/bin/rails +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path("../config/application", __dir__) -require_relative "../config/boot" -require "rails/commands" diff --git a/bin/rake b/bin/rake deleted file mode 100755 index 4fbf10b..0000000 --- a/bin/rake +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -require_relative "../config/boot" -require "rake" -Rake.application.run diff --git a/bin/rubocop b/bin/rubocop deleted file mode 100755 index 40330c0..0000000 --- a/bin/rubocop +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -require "rubygems" -require "bundler/setup" - -# explicit rubocop config increases performance slightly while avoiding config confusion. -ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) - -load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup deleted file mode 100755 index 26ad1cb..0000000 --- a/bin/setup +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env ruby -require "fileutils" - -APP_ROOT = File.expand_path("..", __dir__) -APP_NAME = "buildlight" - -def system!(*args) - system(*args, exception: true) -end - -FileUtils.chdir APP_ROOT do - # This script is a way to set up or update your development environment automatically. - # This script is idempotent, so that you can run it at any time and get an expectable outcome. - # Add necessary setup steps to this file. - - puts "== Installing dependencies ==" - system! "gem install bundler --conservative" - system("bundle check") || system!("bundle install") - - puts "\n== Copying sample files ==" - unless File.exist?("config/application.yml") - FileUtils.cp "config/application.example.yml", "config/application.yml" - end - - puts "\n== Preparing database ==" - system! "bin/rails db:prepare" - - puts "\n== Removing old logs and tempfiles ==" - system! "bin/rails log:clear tmp:clear" - - puts "\n== Restarting application server ==" - system! "bin/rails restart" - - # puts "\n== Configuring puma-dev ==" - # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" - # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" -end diff --git a/bin/thrust b/bin/thrust deleted file mode 100755 index 36bde2d..0000000 --- a/bin/thrust +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby -require "rubygems" -require "bundler/setup" - -load Gem.bin_path("thruster", "thrust") diff --git a/bin/update b/bin/update deleted file mode 100755 index 58bfaed..0000000 --- a/bin/update +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env ruby -require 'fileutils' -include FileUtils - -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) - -def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") -end - -chdir APP_ROOT do - # This script is a way to update your development environment automatically. - # Add necessary update steps to this file. - - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') - - puts "\n== Updating database ==" - system! 'bin/rails db:migrate' - - puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' - - puts "\n== Restarting application server ==" - system! 'bin/rails restart' -end diff --git a/config.ru b/config.ru deleted file mode 100644 index 5e97aa6..0000000 --- a/config.ru +++ /dev/null @@ -1,5 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path("../config/environment", __FILE__) -use Rack::Deflater -run Buildlight::Application diff --git a/config/application.example.yml b/config/application.example.yml deleted file mode 100644 index de3ef42..0000000 --- a/config/application.example.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Add application configuration variables here, as shown below. -# -SECRET_TOKEN: something-random-and-at-least-30-characters -PARTICLE_ACCESS_TOKEN: "abcd5c82f7a646d8337cb575c45d6f9bf2bf12e69" -HOST: locahost:3000 diff --git a/config/application.rb b/config/application.rb deleted file mode 100644 index 744502c..0000000 --- a/config/application.rb +++ /dev/null @@ -1,44 +0,0 @@ -require_relative "boot" - -require "rails" -# Pick the frameworks you want: -require "active_model/railtie" -require "active_job/railtie" -require "active_record/railtie" -# require "active_storage/engine" -require "action_controller/railtie" -require "action_mailer/railtie" -# require "action_mailbox/engine" -# require "action_text/engine" -require "action_view/railtie" -require "action_cable/engine" -# require "rails/test_unit/railtie" - -# Require the gems listed in Gemfile, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(*Rails.groups) - -module Buildlight - class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 - - # Please, add to the `ignore` list any other `lib` subdirectories that do - # not contain `.rb` files, or that should not be reloaded or eager loaded. - # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w[assets tasks]) - - # Configuration for the application, engines, and railties goes here. - # - # These settings can be overridden in specific environments using the files - # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - - config.x.debug = ENV["DEBUG"].present? - config.x.host = ENV["HOST"] - # Don't generate system test files. - config.generators.system_tests = nil - end -end diff --git a/config/boot.rb b/config/boot.rb deleted file mode 100644 index 988a5dd..0000000 --- a/config/boot.rb +++ /dev/null @@ -1,4 +0,0 @@ -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -require "bundler/setup" # Set up gems listed in the Gemfile. -require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml deleted file mode 100644 index dee099f..0000000 --- a/config/cable.yml +++ /dev/null @@ -1,6 +0,0 @@ -development: - adapter: postgresql -test: - adapter: postgresql -production: - adapter: postgresql diff --git a/config/database.yml b/config/database.yml deleted file mode 100644 index e83a93c..0000000 --- a/config/database.yml +++ /dev/null @@ -1,16 +0,0 @@ -default: &default - adapter: postgresql - encoding: unicode - # For details on connection pooling, see rails configuration guide - # http://guides.rubyonrails.org/configuring.html#database-pooling - pool: 15 - -development: - <<: *default - database: buildlight_development - # min_messages: DEBUG5 - -test: - <<: *default - database: buildlight_test - diff --git a/config/dockerfile.yml b/config/dockerfile.yml deleted file mode 100644 index 1928ca5..0000000 --- a/config/dockerfile.yml +++ /dev/null @@ -1,16 +0,0 @@ -# generated by dockerfile-rails - ---- -options: - alpine: true - bin-cd: true - gemfile-updates: false - label: - fly_launch_runtime: rails - parallel: true - prepare: false - packages: - build: - - git - deploy: - - gzip diff --git a/config/environment.rb b/config/environment.rb deleted file mode 100644 index cac5315..0000000 --- a/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the Rails application. -require_relative "application" - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb deleted file mode 100644 index a4c2a23..0000000 --- a/config/environments/development.rb +++ /dev/null @@ -1,69 +0,0 @@ -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Make code changes take effect immediately without server restart. - config.enable_reloading = true - - # Do not eager load code on boot. - config.eager_load = false - - # Show full error reports. - config.consider_all_requests_local = true - - # Enable server timing. - config.server_timing = true - - # Enable/disable Action Controller caching. By default Action Controller caching is disabled. - # Run rails dev:cache to toggle Action Controller caching. - if Rails.root.join("tmp/caching-dev.txt").exist? - config.action_controller.perform_caching = true - config.action_controller.enable_fragment_cache_logging = true - config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"} - else - config.action_controller.perform_caching = false - end - - # Change to :null_store to avoid any caching. - config.cache_store = :memory_store - - # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false - - # Make template changes take effect immediately. - config.action_mailer.perform_caching = false - - # Set localhost to be used by links generated in mailer templates. - config.action_mailer.default_url_options = {host: "localhost", port: 3000} - - # Print deprecation notices to the Rails logger. - config.active_support.deprecation = :log - - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = true - - # Append comments with runtime information tags to SQL queries in logs. - config.active_record.query_log_tags_enabled = true - - # Highlight code that enqueued background job in logs. - config.active_job.verbose_enqueue_logs = true - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - config.action_view.annotate_rendered_view_with_filenames = true - - # Uncomment if you wish to allow Action Cable access from any origin. - # config.action_cable.disable_request_forgery_protection = true - - # Raise error when a before_action's only/except options reference missing actions. - config.action_controller.raise_on_missing_callback_actions = true - - # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. - # config.generators.apply_rubocop_autocorrect_after_generate! -end diff --git a/config/environments/production.rb b/config/environments/production.rb deleted file mode 100644 index ebca867..0000000 --- a/config/environments/production.rb +++ /dev/null @@ -1,89 +0,0 @@ -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.enable_reloading = false - - # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). - config.eager_load = true - - # Full error reports are disabled. - config.consider_all_requests_local = false - - # Turn on fragment caching in view templates. - config.action_controller.perform_caching = true - - # Cache assets for far-future expiry since they are all digest stamped. - config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"} - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - - # Assume all access to the app is happening through a SSL-terminating reverse proxy. - config.assume_ssl = true - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true - - # Skip http-to-https redirect for the default health check endpoint. - # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } - - # Log to STDOUT with the current request id as a default log tag. - config.log_tags = [:request_id] - config.logger = ActiveSupport::TaggedLogging.logger($stdout) - - # Change to "debug" to log everything (including potentially personally-identifiable information!) - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - - # Prevent health checks from clogging up the logs. - config.silence_healthcheck_path = "/up" - - # Don't log any deprecations. - config.active_support.report_deprecations = false - - # Replace the default in-process memory cache store with a durable alternative. - # config.cache_store = :mem_cache_store - - # Replace the default in-process and non-durable queuing backend for Active Job. - # config.active_job.queue_adapter = :resque - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = {host: "example.com"} - - # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. - # config.action_mailer.smtp_settings = { - # user_name: Rails.application.credentials.dig(:smtp, :user_name), - # password: Rails.application.credentials.dig(:smtp, :password), - # address: "smtp.example.com", - # port: 587, - # authentication: :plain - # } - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false - - # Only use :id for inspections in production. - config.active_record.attributes_for_inspect = [:id] - - # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # - # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - - config.action_cable.url = "wss://#{ENV["HOST"]}/cable" - config.action_cable.allowed_request_origins = ["https://#{ENV["HOST"]}", "http://#{ENV["HOST"]}"] -end diff --git a/config/environments/test.rb b/config/environments/test.rb deleted file mode 100644 index 306866b..0000000 --- a/config/environments/test.rb +++ /dev/null @@ -1,50 +0,0 @@ -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # While tests run files are not watched, reloading is not necessary. - config.enable_reloading = false - - # Eager loading loads your entire application. When running a single test locally, - # this is usually not necessary, and can slow down your test suite. However, it's - # recommended that you enable it in continuous integration systems to ensure eager - # loading is working properly before deploying your code. - config.eager_load = ENV["CI"].present? - - # Configure public file server for tests with cache-control for performance. - config.public_file_server.headers = {"cache-control" => "public, max-age=3600"} - - # Show full error reports. - config.consider_all_requests_local = true - config.cache_store = :null_store - - # Render exception templates for rescuable exceptions and raise for other exceptions. - config.action_dispatch.show_exceptions = :rescuable - - # Disable request forgery protection in test environment. - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - - # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = {host: "example.com"} - - # Print deprecation notices to the stderr. - config.active_support.deprecation = :stderr - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Raise error when a before_action's only/except options reference missing actions. - config.action_controller.raise_on_missing_callback_actions = true -end diff --git a/config/importmap.rb b/config/importmap.rb deleted file mode 100644 index bbf1019..0000000 --- a/config/importmap.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Pin npm packages by running ./bin/importmap - -pin "application" -pin "@rails/actioncable", to: "actioncable.esm.js" -pin_all_from "app/javascript/channels", under: "channels" diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb deleted file mode 100644 index 89d2efa..0000000 --- a/config/initializers/application_controller_renderer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# ActiveSupport::Reloader.to_prepare do -# ApplicationController.renderer.defaults.merge!( -# http_host: 'example.org', -# https: false -# ) -# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb deleted file mode 100644 index 4873244..0000000 --- a/config/initializers/assets.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "1.0" - -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 33699c3..0000000 --- a/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code -# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". -Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb deleted file mode 100644 index b3076b3..0000000 --- a/config/initializers/content_security_policy.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Define an application-wide content security policy. -# See the Securing Rails Applications Guide for more information: -# https://guides.rubyonrails.org/security.html#content-security-policy-header - -# Rails.application.configure do -# config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end -# -# # Generate session nonces for permitted importmap, inline scripts, and inline styles. -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src style-src) -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true -# end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb deleted file mode 100644 index 5a6a32d..0000000 --- a/config/initializers/cookies_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Specify a serializer for the signed and encrypted cookie jars. -# Valid options are :json, :marshal, and :hybrid. -Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index c0b717f..0000000 --- a/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. -# Use this to limit dissemination of sensitive information. -# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc -] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb deleted file mode 100644 index fa9b938..0000000 --- a/config/initializers/inflections.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format. Inflections -# are locale specific, and you may define rules for as many different -# locales as you wish. All of these examples are active by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, "\\1en" -# inflect.singular /^(ox)en/i, "\\1" -# inflect.irregular "person", "people" -# inflect.uncountable %w( fish sheep ) -# end - -# These inflection rules are supported but not enabled by default: -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.acronym "API" -end diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb deleted file mode 100644 index d89dac7..0000000 --- a/config/initializers/locale.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. -# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. -# Rails.application.config.time_zone = 'Central Time (US & Canada)' - -# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. -# Rails.application.config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] -# Rails.application.config.i18n.default_locale = :de diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb deleted file mode 100644 index 1e53b37..0000000 --- a/config/initializers/mime_types.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -Mime::Type.register "text/ryg", :ryg -# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb deleted file mode 100644 index c383d07..0000000 --- a/config/initializers/new_framework_defaults_5_2.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -# Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -# Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -# Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -# Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -# Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb deleted file mode 100644 index 92efa95..0000000 --- a/config/initializers/new_framework_defaults_8_0.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 8.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `8.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -### -# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. -# If set to `:zone`, `to_time` methods will use the timezone of their receivers. -# If set to `:offset`, `to_time` methods will use the UTC offset. -# If `false`, `to_time` methods will convert to the local system UTC offset instead. -#++ -# Rails.application.config.active_support.to_time_preserves_timezone = :zone - -### -# When both `If-Modified-Since` and `If-None-Match` are provided by the client -# only consider `If-None-Match` as specified by RFC 7232 Section 6. -# If set to `false` both conditions need to be satisfied. -#++ -# Rails.application.config.action_dispatch.strict_freshness = true - -### -# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. -#++ -# Regexp.timeout = 1 diff --git a/config/initializers/particle.rb b/config/initializers/particle.rb deleted file mode 100644 index 2820074..0000000 --- a/config/initializers/particle.rb +++ /dev/null @@ -1,3 +0,0 @@ -Particle.configure do |c| - c.access_token = ENV["PARTICLE_ACCESS_TOKEN"] -end diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb deleted file mode 100644 index 7db3b95..0000000 --- a/config/initializers/permissions_policy.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Define an application-wide HTTP permissions policy. For further -# information see: https://developers.google.com/web/updates/2018/06/feature-policy - -# Rails.application.config.permissions_policy do |policy| -# policy.camera :none -# policy.gyroscope :none -# policy.microphone :none -# policy.usb :none -# policy.fullscreen :self -# policy.payment :self, "https://secure.example.com" -# end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb deleted file mode 100644 index 5b04c51..0000000 --- a/config/initializers/session_store.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.config.session_store :cookie_store, key: "_buildlight_session" diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb deleted file mode 100644 index bbfc396..0000000 --- a/config/initializers/wrap_parameters.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# This file contains settings for ActionController::ParamsWrapper which -# is enabled by default. - -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] -end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end diff --git a/config/locales/en.yml b/config/locales/en.yml deleted file mode 100644 index decc5a8..0000000 --- a/config/locales/en.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t 'hello' -# -# In views, this is aliased to just `t`: -# -# <%= t('hello') %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: -# -# true, false, on, off, yes, no -# -# Instead, surround them with single quotes. -# -# en: -# 'true': 'foo' -# -# To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. - -en: - hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb deleted file mode 100644 index a248513..0000000 --- a/config/puma.rb +++ /dev/null @@ -1,41 +0,0 @@ -# This configuration file will be evaluated by Puma. The top-level methods that -# are invoked here are part of Puma's configuration DSL. For more information -# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. -# -# Puma starts a configurable number of processes (workers) and each process -# serves each request in a thread from an internal thread pool. -# -# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You -# should only set this value when you want to run 2 or more workers. The -# default is already 1. -# -# The ideal number of threads per worker depends both on how much time the -# application spends waiting for IO operations and on how much you wish to -# prioritize throughput over latency. -# -# As a rule of thumb, increasing the number of threads will increase how much -# traffic a given process can handle (throughput), but due to CRuby's -# Global VM Lock (GVL) it has diminishing returns and will degrade the -# response time (latency) of the application. -# -# The default is set to 3 threads as it's deemed a decent compromise between -# throughput and latency for the average Rails application. -# -# Any libraries that use a connection pool or another resource pool should -# be configured to provide at least as many connections as the number of -# threads. This includes Active Record's `pool` parameter in `database.yml`. -threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) -threads threads_count, threads_count - -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch("PORT", 3000) - -# Allow puma to be restarted by `bin/rails restart` command. -plugin :tmp_restart - -# Run the Solid Queue supervisor inside of Puma for single-server deployments -plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] - -# Specify the PID file. Defaults to tmp/pids/server.pid in development. -# In other environments, only set the PID file if requested. -pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb deleted file mode 100644 index a774ef8..0000000 --- a/config/routes.rb +++ /dev/null @@ -1,19 +0,0 @@ -Rails.application.routes.draw do - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", :as => :rails_health_check - - # use a namespace to avoid resources colliding with usernames - namespace :api do - resources :devices, only: :show - resource :device, only: [] do - post :trigger - get ":id/red" => "red#show" - end - end - - resources :devices, only: :show - get ":id(.:format)" => "colors#show" - get "/(.:format)" => "colors#index" - post "/" => "webhooks#create" -end diff --git a/config/secrets.yml b/config/secrets.yml deleted file mode 100644 index 705ebb0..0000000 --- a/config/secrets.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: 8d0348e17826916dae219e7319e5e7a0a8f359a12b402c1e4276d8f0acc553905ff499b0c27586fe6c3250dc45328abe99be4ba2648b83a6ab88f4d049497ed2 - -test: - secret_key_base: 5897b9e5d46263db0c851b9fe5248b1757aecab847ff72088a88623f21e15ec74bfb525d90ee9b0d687e3c51130904619b920d8552914b9982fae15955760536 - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/config/storage.yml b/config/storage.yml deleted file mode 100644 index d32f76e..0000000 --- a/config/storage.yml +++ /dev/null @@ -1,34 +0,0 @@ -test: - service: Disk - root: <%= Rails.root.join("tmp/storage") %> - -local: - service: Disk - root: <%= Rails.root.join("storage") %> - -# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) -# amazon: -# service: S3 -# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> -# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> -# region: us-east-1 -# bucket: your_own_bucket - -# Remember not to checkin your GCS keyfile to a repository -# google: -# service: GCS -# project: your_project -# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket - -# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) -# microsoft: -# service: AzureStorage -# storage_account_name: your_account_name -# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name - -# mirror: -# service: Mirror -# primary: local -# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20121123160543_create_statuses.rb b/db/migrate/20121123160543_create_statuses.rb deleted file mode 100644 index 3e43483..0000000 --- a/db/migrate/20121123160543_create_statuses.rb +++ /dev/null @@ -1,17 +0,0 @@ -class CreateStatuses < ActiveRecord::Migration[4.2] - def change - create_table :statuses do |t| - t.string :project_id - t.string :project_name - t.string :status - - t.timestamps - end - - add_index :statuses, :project_id - add_index :statuses, :project_name - add_index :statuses, :status - add_index :statuses, [:project_id, :status] - add_index :statuses, [:project_id, :status, :created_at] - end -end diff --git a/db/migrate/20121123172057_add_payload_to_statuses.rb b/db/migrate/20121123172057_add_payload_to_statuses.rb deleted file mode 100644 index f1d342f..0000000 --- a/db/migrate/20121123172057_add_payload_to_statuses.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddPayloadToStatuses < ActiveRecord::Migration[4.2] - def change - add_column :statuses, :payload, :text - end -end diff --git a/db/migrate/20121123182506_rename_status_to_color_on_statuses.rb b/db/migrate/20121123182506_rename_status_to_color_on_statuses.rb deleted file mode 100644 index ec0c933..0000000 --- a/db/migrate/20121123182506_rename_status_to_color_on_statuses.rb +++ /dev/null @@ -1,13 +0,0 @@ -class RenameStatusToColorOnStatuses < ActiveRecord::Migration[4.2] - def change - remove_index :statuses, :status - remove_index :statuses, [:project_id, :status] - remove_index :statuses, [:project_id, :status, :created_at] - - rename_column :statuses, :status, :color - - add_index :statuses, :color - add_index :statuses, [:project_id, :color] - add_index :statuses, [:project_id, :color, :created_at] - end -end diff --git a/db/migrate/20121123195427_split_colors_on_statuses.rb b/db/migrate/20121123195427_split_colors_on_statuses.rb deleted file mode 100644 index 68bbabd..0000000 --- a/db/migrate/20121123195427_split_colors_on_statuses.rb +++ /dev/null @@ -1,16 +0,0 @@ -class SplitColorsOnStatuses < ActiveRecord::Migration[4.2] - def change - change_table :statuses, bulk: true do |t| - t.remove_index :color - t.remove_index [:project_id, :color] - t.remove_index [:project_id, :color, :created_at] - - t.boolean :red - t.boolean :yellow - t.remove :color, type: :string - - t.index :red - t.index :yellow - end - end -end diff --git a/db/migrate/20121124190606_add_user_to_statuses.rb b/db/migrate/20121124190606_add_user_to_statuses.rb deleted file mode 100644 index 176b9a8..0000000 --- a/db/migrate/20121124190606_add_user_to_statuses.rb +++ /dev/null @@ -1,9 +0,0 @@ -class AddUserToStatuses < ActiveRecord::Migration[4.2] - def change - add_column :statuses, :username, :string - add_index :statuses, :username - add_index :statuses, [:username, :project_name] - add_index :statuses, [:username, :red] - add_index :statuses, [:username, :yellow] - end -end diff --git a/db/migrate/20160510201736_create_devices.rb b/db/migrate/20160510201736_create_devices.rb deleted file mode 100644 index da6c378..0000000 --- a/db/migrate/20160510201736_create_devices.rb +++ /dev/null @@ -1,12 +0,0 @@ -class CreateDevices < ActiveRecord::Migration[4.2] - def change - enable_extension "uuid-ossp" - - create_table :devices, id: :uuid do |t| - t.string :usernames, array: true, default: [], null: false - t.string :projects, array: true, default: [], null: false - - t.timestamps null: false - end - end -end diff --git a/db/migrate/20160510212722_add_identifier_to_devices.rb b/db/migrate/20160510212722_add_identifier_to_devices.rb deleted file mode 100644 index 7962ad3..0000000 --- a/db/migrate/20160510212722_add_identifier_to_devices.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AddIdentifierToDevices < ActiveRecord::Migration[4.2] - def change - add_column :devices, :identifier, :string, null: false - add_index :devices, :identifier, unique: true - end -end diff --git a/db/migrate/20160510213407_add_name_to_devices.rb b/db/migrate/20160510213407_add_name_to_devices.rb deleted file mode 100644 index eebd82d..0000000 --- a/db/migrate/20160510213407_add_name_to_devices.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AddNameToDevices < ActiveRecord::Migration[4.2] - def change - add_column :devices, :name, :string, null: false - add_index :devices, :name - end -end diff --git a/db/migrate/20161012193415_add_service_to_status.rb b/db/migrate/20161012193415_add_service_to_status.rb deleted file mode 100644 index 865c12e..0000000 --- a/db/migrate/20161012193415_add_service_to_status.rb +++ /dev/null @@ -1,14 +0,0 @@ -class AddServiceToStatus < ActiveRecord::Migration[5.0] - class Status < ApplicationRecord - end - - def up - add_column :statuses, :service, :string - Status.update_all(service: "travis") - change_column :statuses, :service, :string, null: false - end - - def down - remove_column :statuses, :service - end -end diff --git a/db/migrate/20230303181951_add_webhook_url_to_devices.rb b/db/migrate/20230303181951_add_webhook_url_to_devices.rb deleted file mode 100644 index 8ce3cca..0000000 --- a/db/migrate/20230303181951_add_webhook_url_to_devices.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddWebhookUrlToDevices < ActiveRecord::Migration[7.0] - def change - add_column :devices, :webhook_url, :string - end -end diff --git a/db/migrate/20230304135152_add_slug_to_devices.rb b/db/migrate/20230304135152_add_slug_to_devices.rb deleted file mode 100644 index 9358722..0000000 --- a/db/migrate/20230304135152_add_slug_to_devices.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AddSlugToDevices < ActiveRecord::Migration[7.0] - def change - enable_extension :citext - add_column :devices, :slug, :citext, null: true - add_index :devices, :slug, unique: true - end -end diff --git a/db/migrate/20230304144230_make_identifier_nullable_on_devices.rb b/db/migrate/20230304144230_make_identifier_nullable_on_devices.rb deleted file mode 100644 index 106f799..0000000 --- a/db/migrate/20230304144230_make_identifier_nullable_on_devices.rb +++ /dev/null @@ -1,5 +0,0 @@ -class MakeIdentifierNullableOnDevices < ActiveRecord::Migration[7.0] - def change - change_column_null :devices, :identifier, true - end -end diff --git a/db/migrate/20230305131208_add_status_to_devices.rb b/db/migrate/20230305131208_add_status_to_devices.rb deleted file mode 100644 index a4fd393..0000000 --- a/db/migrate/20230305131208_add_status_to_devices.rb +++ /dev/null @@ -1,8 +0,0 @@ -class AddStatusToDevices < ActiveRecord::Migration[7.0] - def change - change_table :devices, bulk: true do |t| - t.string :status - t.datetime :status_changed_at - end - end -end diff --git a/db/migrate/20230311115915_add_workflow_to_statues.rb b/db/migrate/20230311115915_add_workflow_to_statues.rb deleted file mode 100644 index 14073ee..0000000 --- a/db/migrate/20230311115915_add_workflow_to_statues.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddWorkflowToStatues < ActiveRecord::Migration[7.0] - def change - add_column :statuses, :workflow, :string - end -end diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index a834327..0000000 --- a/db/schema.rb +++ /dev/null @@ -1,56 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# This file is the source Rails uses to define your schema when running `bin/rails -# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to -# be faster and is potentially less error prone than running all of your -# migrations from scratch. Old migrations may fail to apply correctly if those -# migrations use external dependencies or application code. -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema[7.0].define(version: 2023_03_11_115915) do - # These are extensions that must be enabled in order to support this database - enable_extension "citext" - enable_extension "plpgsql" - enable_extension "uuid-ossp" - - create_table "devices", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| - t.string "usernames", default: [], null: false, array: true - t.string "projects", default: [], null: false, array: true - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "identifier" - t.string "name", null: false - t.string "webhook_url" - t.citext "slug" - t.string "status" - t.datetime "status_changed_at" - t.index ["identifier"], name: "index_devices_on_identifier", unique: true - t.index ["name"], name: "index_devices_on_name" - t.index ["slug"], name: "index_devices_on_slug", unique: true - end - - create_table "statuses", force: :cascade do |t| - t.string "project_id" - t.string "project_name" - t.datetime "created_at" - t.datetime "updated_at" - t.text "payload" - t.boolean "red" - t.boolean "yellow" - t.string "username" - t.string "service", null: false - t.string "workflow" - t.index ["project_id"], name: "index_statuses_on_project_id" - t.index ["project_name"], name: "index_statuses_on_project_name" - t.index ["red"], name: "index_statuses_on_red" - t.index ["username", "project_name"], name: "index_statuses_on_username_and_project_name" - t.index ["username", "red"], name: "index_statuses_on_username_and_red" - t.index ["username", "yellow"], name: "index_statuses_on_username_and_yellow" - t.index ["username"], name: "index_statuses_on_username" - t.index ["yellow"], name: "index_statuses_on_yellow" - end - -end diff --git a/db/seeds.rb b/db/seeds.rb deleted file mode 100644 index 4edb1e8..0000000 --- a/db/seeds.rb +++ /dev/null @@ -1,7 +0,0 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). -# -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP deleted file mode 100644 index fe41f5c..0000000 --- a/doc/README_FOR_APP +++ /dev/null @@ -1,2 +0,0 @@ -Use this README file to introduce your application and point to useful places in the API for learning more. -Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/fly.toml b/fly.toml index 1051617..3db0cb6 100644 --- a/fly.toml +++ b/fly.toml @@ -1,21 +1,11 @@ -# fly.toml app configuration file generated for buildlight on 2024-08-01T14:11:40-04:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - app = 'buildlight' primary_region = 'iad' -console_command = '/rails/bin/rails console' [build] -[deploy] - release_command = "./bin/rails db:prepare" - [env] HOST = 'buildlight.collectiveidea.com' PORT = '8080' - RUBYOPT = '--enable=frozen-string-literal' [http_service] internal_port = 8080 @@ -36,7 +26,7 @@ console_command = '/rails/bin/rails console' memory = '256mb' cpu_kind = 'shared' cpus = 1 + [[statics]] - guest_path = "/rails/public" + guest_path = "/app/public" url_prefix = "/" - diff --git a/lib/assets/.gitkeep b/lib/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/log/.gitkeep b/log/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/20230311115915_initial_schema.down.sql b/migrations/20230311115915_initial_schema.down.sql new file mode 100644 index 0000000..5c1405b --- /dev/null +++ b/migrations/20230311115915_initial_schema.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS devices; +DROP TABLE IF EXISTS statuses; diff --git a/migrations/20230311115915_initial_schema.up.sql b/migrations/20230311115915_initial_schema.up.sql new file mode 100644 index 0000000..ef6584d --- /dev/null +++ b/migrations/20230311115915_initial_schema.up.sql @@ -0,0 +1,43 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS citext; + +CREATE TABLE statuses ( + id BIGSERIAL PRIMARY KEY, + project_id VARCHAR, + project_name VARCHAR, + created_at TIMESTAMP, + updated_at TIMESTAMP, + payload TEXT, + red BOOLEAN, + yellow BOOLEAN, + username VARCHAR, + service VARCHAR NOT NULL, + workflow VARCHAR +); + +CREATE INDEX index_statuses_on_project_id ON statuses (project_id); +CREATE INDEX index_statuses_on_project_name ON statuses (project_name); +CREATE INDEX index_statuses_on_red ON statuses (red); +CREATE INDEX index_statuses_on_yellow ON statuses (yellow); +CREATE INDEX index_statuses_on_username ON statuses (username); +CREATE INDEX index_statuses_on_username_and_project_name ON statuses (username, project_name); +CREATE INDEX index_statuses_on_username_and_red ON statuses (username, red); +CREATE INDEX index_statuses_on_username_and_yellow ON statuses (username, yellow); + +CREATE TABLE devices ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + usernames VARCHAR[] NOT NULL DEFAULT '{}', + projects VARCHAR[] NOT NULL DEFAULT '{}', + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + identifier VARCHAR, + name VARCHAR NOT NULL, + webhook_url VARCHAR, + slug CITEXT, + status VARCHAR, + status_changed_at TIMESTAMP +); + +CREATE UNIQUE INDEX index_devices_on_identifier ON devices (identifier); +CREATE INDEX index_devices_on_name ON devices (name); +CREATE UNIQUE INDEX index_devices_on_slug ON devices (slug); diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..ff4f5f4 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +"cargo:sqlx-cli" = { version = "latest", features = "postgres" } +rust = "latest" diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5ce8282..0000000 --- a/package-lock.json +++ /dev/null @@ -1,494 +0,0 @@ -{ - "name": "app", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "app", - "dependencies": { - "sass": "^1.83.4" - }, - "engines": { - "node": "^22.0.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/immutable": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", - "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 676b57b..0000000 --- a/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "app", - "private": "true", - "dependencies": { - "sass": "^1.83.4" - }, - "engines": { - "node": "^22.0.0" - }, - "scripts": { - "build:css": "sass ./app/assets/stylesheets/application.sass.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules" - } -} diff --git a/public/css/application.css b/public/css/application.css new file mode 100644 index 0000000..bb2c2a3 --- /dev/null +++ b/public/css/application.css @@ -0,0 +1,277 @@ +@charset "utf-8"; + +/* Variables */ +:root { + --black: #000; + --white: #fff; + --red: #cf4c29; + --yellow: #ecc561; + --green: #28b56e; + + --base-background-color: color-mix(in srgb, var(--black) 75%, var(--white)); + --base-text-color: color-mix(in srgb, var(--black) 10%, var(--white)); + --strong-text-color: var(--white); + --muted-text-color: color-mix(in srgb, var(--black) 50%, var(--white)); + + --base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + + --base-transition-speed: 500ms; + --fast-transition-speed: 250ms; + --slow-transition-speed: 1000ms; + --slug-transition-speed: 4000ms; + + --bulb-size: 50vmin; + --bulb-shadow-size: 150vmax; + --bulb-gutter: 6vmin; + --light-height: calc(var(--bulb-size) * 3 + var(--bulb-gutter) * 2); + + --animation-pulse-duration: 5s; +} + +/* Reset */ +html { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +a { + background-color: transparent; + -webkit-text-decoration-skip: objects; + + &:active, + &:hover { + outline-width: 0; + } +} + +/* Elements */ +html { + font-family: var(--base-font-family); + font-weight: 300; + line-height: 1.4; +} + +body { + background-color: var(--base-background-color); + color: var(--base-text-color); + font-size: 1em; +} + +ol, +ul { + padding-left: 1rem; +} + +a { + color: currentColor; + + &:focus, + &:hover { + color: var(--strong-text-color); + } +} + +/* Keyframes */ +@keyframes pulse { + 50% { transform: scale(.8); } +} + +/* Bulb */ +.bulb { + height: var(--bulb-size); + position: relative; + width: var(--bulb-size); + + &__glow { + animation: pulse var(--animation-pulse-duration) infinite; + display: block; + height: var(--bulb-shadow-size); + left: 50%; + margin-left: calc(var(--bulb-shadow-size) * -0.5); + margin-top: calc(var(--bulb-shadow-size) * -0.5); + opacity: 0; + position: absolute; + top: 50%; + transform: scale(.5); + transition: transform var(--slow-transition-speed), opacity var(--slow-transition-speed); + transition-delay: var(--fast-transition-speed); + width: var(--bulb-shadow-size); + z-index: 2; + } + + &__disc { + background-color: rgba(0, 0, 0, .2); + border-radius: 50%; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: background-color var(--fast-transition-speed); + z-index: 3; + } + + &__text { + font-size: calc(var(--bulb-size) / 7); + font-weight: 800; + left: calc(var(--bulb-size) * 0.5); + line-height: 1; + margin-left: calc(var(--bulb-size) * -0.5); + margin-top: calc(var(--bulb-size) / 7 * -0.5); + opacity: 0; + position: absolute; + text-align: center; + text-transform: lowercase; + top: calc(var(--bulb-size) * 0.5); + transition: opacity var(--base-transition-speed); + width: var(--bulb-size); + z-index: 4; + } +} + +/* Yellow glow sits underneath other glows */ +.bulb--yellow .bulb__glow { + z-index: 1; + animation-delay: calc(var(--animation-pulse-duration) * -0.5); +} + +/* Red bulb active state */ +[data-failing] .bulb--red { + & .bulb__glow { + background-image: radial-gradient( + ellipse at center, + rgba(207, 76, 41, .9) 10%, + rgba(207, 76, 41, .6) 20%, + rgba(207, 76, 41, .3) 40%, + rgba(207, 76, 41, 0) 70% + ); + opacity: 1; + transform: scale(1); + } + + & .bulb__disc { background-color: var(--red); } + & .bulb__text { opacity: 1; } +} + +/* Yellow bulb active state */ +[data-building] .bulb--yellow { + & .bulb__glow { + background-image: radial-gradient( + ellipse at center, + rgba(236, 197, 97, .9) 10%, + rgba(236, 197, 97, .6) 20%, + rgba(236, 197, 97, .3) 40%, + rgba(236, 197, 97, 0) 70% + ); + opacity: 1; + transform: scale(1); + } + + & .bulb__disc { background-color: var(--yellow); } + & .bulb__text { opacity: 1; } +} + +/* Green bulb active state */ +[data-passing] .bulb--green { + & .bulb__glow { + background-image: radial-gradient( + ellipse at center, + rgba(40, 181, 110, .9) 10%, + rgba(40, 181, 110, .6) 20%, + rgba(40, 181, 110, .3) 40%, + rgba(40, 181, 110, 0) 70% + ); + opacity: 1; + transform: scale(1); + } + + & .bulb__disc { background-color: var(--green); } + & .bulb__text { opacity: 1; } +} + +/* Traffic light */ +.light { + bottom: 0; + left: 0; + overflow: hidden; + position: absolute; + right: 0; + top: 0; + + &__box { + display: flex; + flex-direction: column; + height: var(--light-height); + justify-content: space-between; + left: calc(50vw - var(--bulb-size) / 2); + position: absolute; + top: calc(50vh - var(--light-height) / 2); + transition: margin-top var(--base-transition-speed); + width: var(--bulb-size); + } +} + +[data-failing] .light__box { + margin-top: calc(var(--bulb-size) + var(--bulb-gutter)); +} + +[data-failing][data-building] .light__box { + margin-top: calc((var(--bulb-size) + var(--bulb-gutter)) * 0.5); +} + +[data-passing] .light__box { + margin-top: calc((var(--bulb-size) + var(--bulb-gutter)) * -1); +} + +[data-passing][data-building] .light__box { + margin-top: calc((var(--bulb-size) + var(--bulb-gutter)) * -0.5); +} + +[data-failing][data-passing] .light__box { + margin-top: 0; + transform: scale(.75); +} + +/* Messages */ +.messages { + left: 5vmin; + position: absolute; + top: 5vmin; + z-index: 10; + + &__footer { + font-size: .8em; + opacity: .5; + + & :first-child { margin-top: 0; } + } +} + +.message { + font-size: 0; + overflow: hidden; + padding-bottom: .5em; + transition: font-size var(--base-transition-speed), + opacity var(--base-transition-speed); + + & > * { + margin-top: 0; + } + + & > :first-child { + font-size: 1.5em; + line-height: 1.2; + margin-bottom: .25em; + } +} + +[data-failing] .message--failure, +[data-building] .message--building, +[data-passing] .message--passing { + font-size: 1em; + opacity: 1; +} diff --git a/public/js/application.js b/public/js/application.js new file mode 100644 index 0000000..9b1fd77 --- /dev/null +++ b/public/js/application.js @@ -0,0 +1,81 @@ +// BuildLight WebSocket client +// Replaces ActionCable with plain WebSocket + +(function () { + // Determine channel and ID from URL path + let channel, ids; + + if (document.location.pathname.match(/^\/devices\//)) { + ids = document.location.pathname.match(/^\/devices\/([^\/\?]*)/)[1].split(","); + channel = "device"; + } else { + let match = document.location.pathname.match(/^\/([^\/\?]*)/); + ids = match ? match[1].split(",") : [""]; + channel = "colors"; + } + + ids.forEach(function (id) { + if (id === "") { + id = "*"; + } + + let protocol = document.location.protocol === "https:" ? "wss:" : "ws:"; + let wsUrl = protocol + "//" + document.location.host + "/ws?channel=" + encodeURIComponent(channel) + "&id=" + encodeURIComponent(id); + + function connect() { + let ws = new WebSocket(wsUrl); + + ws.onopen = function () { + console.log("BuildLight WebSocket connected"); + }; + + ws.onmessage = function (event) { + let data = JSON.parse(event.data); + updateUI(data); + }; + + ws.onclose = function () { + // Reconnect after 3 seconds + setTimeout(connect, 3000); + }; + + ws.onerror = function () { + ws.close(); + }; + } + + connect(); + }); + + function updateUI(data) { + let redCount = +data.colors.red; + let favicon = redCount ? "/favicon-failing" : "/favicon-passing"; + + if (redCount > 0) { + document.body.setAttribute("data-failing", ""); + document.body.removeAttribute("data-passing"); + let count = document.getElementById("failing-count"); + if (count) { + let message = redCount === 1 + ? redCount + " project is" + : redCount + " projects are"; + count.textContent = message; + } + } else { + document.body.removeAttribute("data-failing"); + document.body.setAttribute("data-passing", ""); + } + + if (data.colors.yellow) { + document.body.setAttribute("data-building", ""); + favicon += "-building"; + } else { + document.body.removeAttribute("data-building"); + } + + let faviconEl = document.getElementById("favicon"); + if (faviconEl) { + faviconEl.setAttribute("href", favicon + ".ico"); + } + } +})(); diff --git a/spec/controllers/api/devices_controller_spec.rb b/spec/controllers/api/devices_controller_spec.rb deleted file mode 100644 index 809d25d..0000000 --- a/spec/controllers/api/devices_controller_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require "rails_helper" - -describe API::DevicesController do - describe "GET show" do - it "returns the colors" do - device = FactoryBot.create(:device, usernames: ["test"]) - FactoryBot.create(:status, username: "test", red: false, yellow: true) - - get :show, params: {id: device.id} - expect(response.status).to eq(200) - expect(response.body).to eq({colors: {red: false, yellow: true, green: true}, ryg: "rYG"}.to_json) - end - end - - describe "POST trigger" do - before do - FactoryBot.create(:device, identifier: "abc123") - allow(Particle).to receive(:publish) - end - - it "notifies Particle" do - expect(Particle).to receive(:publish).with(name: "build_state", data: "passing", ttl: 3600, private: false) - post :trigger, params: {name: "ready", data: "true", coreid: "abc123", published_at: "2016-06-14T22:06:10.976Z"} - end - - it "does not notify if there is no device" do - expect(Particle).not_to receive(:publish) - post :trigger, params: {name: "ready", data: "true", coreid: "FAKE", published_at: "2016-06-14T22:06:10.976Z"} - end - end -end diff --git a/spec/controllers/api/red_controller_spec.rb b/spec/controllers/api/red_controller_spec.rb deleted file mode 100644 index b1254de..0000000 --- a/spec/controllers/api/red_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "rails_helper" - -describe API::RedController do - let!(:red1) { FactoryBot.create :status, red: true, username: "user1" } - let!(:red2) { FactoryBot.create :status, red: true, username: "user2" } - let!(:green1) { FactoryBot.create :status, username: "user1" } - let!(:green2) { FactoryBot.create :status, username: "user2" } - - describe "#show" do - render_views - - let!(:device) { FactoryBot.create(:device, identifier: "abc123", usernames: ["user1"]) } - - it "responds with the list of red project names" do - get :show, params: {id: "abc123"} - - expect(response.body).to match(/#{red1.project_name}/) - expect(response.body).not_to match(/#{red2.project_name}/) - expect(response.body).not_to match(/#{green1.project_name}/) - expect(response.body).not_to match(/#{green2.project_name}/) - end - - it "responds with the list of red projects serialized as json" do - get :show, params: {id: "abc123", format: :json} - - response_json = JSON.parse(response.body) - expect(response_json).to match_array([{"project_name" => red1.project_name, "username" => red1.username}]) - end - end -end diff --git a/spec/controllers/colors_controller_spec.rb b/spec/controllers/colors_controller_spec.rb deleted file mode 100644 index 32b6d5b..0000000 --- a/spec/controllers/colors_controller_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "rails_helper" - -describe ColorsController do - describe "index" do - it "shows the red light on if the last status is red" do - FactoryBot.create :status, red: true - get :index, params: {format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be_truthy - end - - it "shows the red and yellow lights on if the last status is yellow, but previous non-yellow was red" do - FactoryBot.create :status, red: true - 2.times { FactoryBot.create :status, red: false, yellow: true } - get :index, params: {format: :json} - json = JSON.parse(response.body) - expect(json["yellow"]).to be(true) - expect(json["red"]).to be_truthy - end - - it "shows the red light on if the last status is green, but another project is red" do - FactoryBot.create :status, red: true - FactoryBot.create :status, red: false - get :index, params: {format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be_truthy - end - end - - describe "show" do - it "shows the status for a single user" do - FactoryBot.create :status, username: "collectiveidea", red: false - FactoryBot.create :status, username: "danielmorrison", red: true - get :show, params: {id: "collectiveidea", format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be(false) - end - - it "shows the status for all users separated by a comma" do - FactoryBot.create :status, username: "collectiveidea", red: false, yellow: true - FactoryBot.create :status, username: "danielmorrison", red: true, yellow: false - get :show, params: {id: "collectiveidea,danielmorrison", format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be_truthy - expect(json["yellow"]).to be(true) - end - end -end diff --git a/spec/controllers/devices_controller_spec.rb b/spec/controllers/devices_controller_spec.rb deleted file mode 100644 index 362857b..0000000 --- a/spec/controllers/devices_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "rails_helper" - -describe DevicesController do - describe "show" do - before do - FactoryBot.create :status, username: "collectiveidea", red: false - FactoryBot.create :status, username: "danielmorrison", red: true - @device = FactoryBot.create :device, usernames: ["collectiveidea"] - end - - it "shows the status for a single device by id" do - get :show, params: {id: @device.id, format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be(false) - end - - it "shows the status for a single device by slug" do - get :show, params: {id: @device.slug, format: :json} - json = JSON.parse(response.body) - expect(json["red"]).to be(false) - end - end -end diff --git a/spec/controllers/webhooks_controller_spec.rb b/spec/controllers/webhooks_controller_spec.rb deleted file mode 100644 index c40ac7d..0000000 --- a/spec/controllers/webhooks_controller_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require "rails_helper" - -describe WebhooksController do - describe "POST create" do - before do - allow(Particle).to receive(:publish) - end - - describe "unknown data" do - it "ignores non-useful data" do - expect(Status.count).to eq(0) - data = {foo: "bar"} - post :create, params: data - expect(response.code).to eq("400") - expect(Status.count).to eq(0) - end - end - - describe "from Travis CI" do - it "recieves a json payload" do - post :create, params: {payload: json_fixture("travis.json")} - expect(response).to be_successful - end - - it "saves useful data" do - post :create, params: {payload: json_fixture("travis.json")} - status = Status.order("created_at DESC").first - expect(status.red).to be(false) - expect(status.service).to eq("travis") - expect(status.project_id).to eq("347744") - expect(status.project_name).to eq("buildlight") - expect(status.username).to eq("collectiveidea") - end - - it "ignores pull requests" do - expect(Status.count).to eq(0) - post :create, params: {payload: json_fixture("travis.json").sub(%("type":"push"), %("type":"pull_request"))} - expect(Status.count).to eq(0) - end - - it "notifies Particle" do - FactoryBot.create(:device, :with_identifier, usernames: ["collectiveidea"]) - allow(Particle).to receive(:publish) - post :create, params: {payload: json_fixture("travis.json")} - expect(Particle).to have_received(:publish).with(name: "build_state", data: "passing", ttl: 3600, private: false) - end - end - - describe "from GitHub Actions" do - it "recieves a json payload" do - post :create, params: JSON.parse(json_fixture("github.json")) - expect(response).to be_successful - end - - it "saves useful data" do - post :create, params: JSON.parse(json_fixture("github.json")) - status = Status.order("created_at DESC").first - expect(status.red).to be(false) - expect(status.service).to eq("github") - expect(status.project_id).to be_nil - expect(status.project_name).to eq("buildlight") - expect(status.username).to eq("collectiveidea") - end - - it "notifies Particle" do - FactoryBot.create(:device, :with_identifier, usernames: ["collectiveidea"]) - allow(Particle).to receive(:publish) - post :create, params: JSON.parse(json_fixture("github.json")) - expect(Particle).to have_received(:publish).with(name: "build_state", data: "passing", ttl: 3600, private: false) - end - end - end -end diff --git a/spec/factories.rb b/spec/factories.rb deleted file mode 100644 index 7716da4..0000000 --- a/spec/factories.rb +++ /dev/null @@ -1,20 +0,0 @@ -FactoryBot.define do - factory :device do - usernames { [] } - projects { [] } - sequence(:name) { |i| "Device #{i}" } - sequence(:slug) { |i| "slug-#{i}" } - - trait :with_identifier do - sequence(:identifier) { |i| "device-#{i}" } - end - end - - factory :status do - service { "travis" } - sequence(:project_id, &:to_s) - sequence(:project_name) { |i| "buildlight#{i}" } - red { false } - yellow { false } - end -end diff --git a/spec/interactors/parse_circle_spec.rb b/spec/interactors/parse_circle_spec.rb deleted file mode 100644 index 126bb09..0000000 --- a/spec/interactors/parse_circle_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "rails_helper" - -describe ParseCircle do - describe "set_colors" do - before do - @status = Status.new(service: "circle") - end - - it "sets success to green" do - ParseCircle.set_colors(@status, "success") - expect(@status.red).to be(false) - expect(@status.yellow).to be(false) - end - - it "sets failed to red" do - ParseCircle.set_colors(@status, "failed") - expect(@status.red).to be(true) - expect(@status.yellow).to be(false) - end - end -end diff --git a/spec/interactors/parse_github_spec.rb b/spec/interactors/parse_github_spec.rb deleted file mode 100644 index 99ef94e..0000000 --- a/spec/interactors/parse_github_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "rails_helper" - -describe ParseGithub do - describe "call" do - it "uses worflow column to differentiate between statuses" do - other_status = FactoryBot.create :status, service: "github", username: "collectiveidea", project_name: "buildlight", workflow: "Other Workflow", red: true - ParseGithub.call(JSON.parse(json_fixture("github.json"))) - expect(other_status.reload.red).to be(true) - expect(Status.where(service: "github", username: "collectiveidea", project_name: "buildlight").count).to eq(2) - end - end - - describe "set_colors" do - before do - @status = Status.new(service: "github") - end - - it "sets 'success' to green" do - ParseGithub.set_colors(@status, "success") - expect(@status.red).to be(false) - expect(@status.yellow).to be(false) - end - - it "sets 'failure' to red" do - ParseGithub.set_colors(@status, "failure") - expect(@status.red).to be(true) - expect(@status.yellow).to be(false) - end - - it "sets '' to yellow" do - ParseGithub.set_colors(@status, "") - expect(@status.yellow).to be(true) - end - - it "keeps the red color if yellow" do - @status.red = true - ParseGithub.set_colors(@status, "") - expect(@status.red).to be(true) - end - end -end diff --git a/spec/interactors/parse_travis_spec.rb b/spec/interactors/parse_travis_spec.rb deleted file mode 100644 index c673126..0000000 --- a/spec/interactors/parse_travis_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require "rails_helper" - -describe ParseTravis do - describe "set_colors" do - before do - @status = Status.new - end - - it "sets Passed to green" do - ParseTravis.set_colors(@status, "Passed") - expect(@status.red).to be(false) - expect(@status.yellow).to be(false) - end - - it "sets Fixed to green" do - ParseTravis.set_colors(@status, "Fixed") - expect(@status.red).to be(false) - expect(@status.yellow).to be(false) - end - - it "sets Still Failing to red" do - ParseTravis.set_colors(@status, "Still Failing") - expect(@status.red).to be(true) - expect(@status.yellow).to be(false) - end - - it "sets Pending to yellow" do - ParseTravis.set_colors(@status, "Pending") - expect(@status.yellow).to be(true) - end - - it "keeps the red color if yellow" do - @status.red = true - ParseTravis.set_colors(@status, "Pending") - expect(@status.red).to be(true) - end - end -end diff --git a/spec/interactors/trigger_webhook_spec.rb b/spec/interactors/trigger_webhook_spec.rb deleted file mode 100644 index 1231e61..0000000 --- a/spec/interactors/trigger_webhook_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "rails_helper" - -describe TriggerWebhook do - describe "triggering a webhook" do - let!(:status) { FactoryBot.create(:status, username: "hooks", project_name: "buildlight") } - let!(:device) { FactoryBot.create(:device, usernames: ["hooks"]) } - - it "it sends a basic webhook" do - # Add webhook without triggering callbacks - device.update_column(:webhook_url, "https://localhost/fake/path") - - allow(Faraday).to receive(:post) - TriggerWebhook.call(device) - expect(Faraday).to have_received(:post).with( - "https://localhost/fake/path", - {colors: {red: false, yellow: false, green: true}}.to_json, - {"Content-Type": "application/json", "x-ryg": "ryG", "x-device-url": "http://locahost:3000/api/devices/#{device.id}"} - ) - end - end -end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb deleted file mode 100644 index ee85328..0000000 --- a/spec/models/device_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require "rails_helper" - -RSpec.describe Device, type: :model do - describe "#statuses" do - let!(:status1) { FactoryBot.create(:status, username: "collectiveidea", project_name: "foo") } - let!(:status2) { FactoryBot.create(:status, username: "collectiveidea", project_name: "bar") } - let!(:status3) { FactoryBot.create(:status, username: "deadmanssnitch", project_name: "foo") } - let!(:status4) { FactoryBot.create(:status, username: "deadmanssnitch", project_name: "bar") } - let!(:status5) { FactoryBot.create(:status, username: "inchworm", project_name: "foo") } - - it "includes status by project" do - device = FactoryBot.create(:device, usernames: [], projects: ["collectiveidea/bar", "deadmanssnitch/foo"]) - - expect(device.statuses.size).to eq(2) - expect(device.statuses).to include(status2) - expect(device.statuses).to include(status3) - end - - it "includes status by username" do - device = FactoryBot.create(:device, usernames: ["collectiveidea", "inchworm"], projects: []) - - expect(device.statuses.size).to eq(3) - expect(device.statuses).to include(status1) - expect(device.statuses).to include(status2) - expect(device.statuses).to include(status5) - end - - it "includes status by username and project at the same time" do - device = FactoryBot.create(:device, usernames: ["collectiveidea"], projects: ["deadmanssnitch/bar"]) - - expect(device.statuses.size).to eq(3) - expect(device.statuses).to include(status1) - expect(device.statuses).to include(status2) - expect(device.statuses).to include(status4) - end - end - - describe "#status" do - it "returns the status for the device" do - FactoryBot.create(:status, username: "collectiveidea", project_name: "foo", red: false, yellow: false) - FactoryBot.create(:status, username: "collectiveidea", project_name: "bar", red: false, yellow: true) - FactoryBot.create(:status, username: "deadmanssnitch", project_name: "foo", red: false, yellow: false) - FactoryBot.create(:status, username: "deadmanssnitch", project_name: "bar", red: true, yellow: true) - - device = FactoryBot.create(:device, usernames: ["collectiveidea"], projects: ["deadmanssnitch/foo"]) - expect(device.reload.status).to eq("passing-building") - - FactoryBot.create(:status, username: "collectiveidea", project_name: "baz", red: true, yellow: false) - expect(device.reload.status).to eq("failing-building") - end - end - - describe "#trigger" do - context "when the device has a webhook_url" do - it "sends a webhook" do - device = FactoryBot.create(:device) - # Add webhook without triggering callbacks - device.update_column(:webhook_url, "https://localhost/fake/path") - - allow(TriggerWebhook).to receive(:call) - device.trigger - expect(TriggerWebhook).to have_received(:call).with(device) - end - end - - context "when the device has an identifier" do - it "sends a webhook" do - device = FactoryBot.create(:device, identifier: "fake") - allow(TriggerParticle).to receive(:call) - device.trigger - expect(TriggerParticle).to have_received(:call).with(device) - end - end - end -end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb deleted file mode 100644 index 37801a5..0000000 --- a/spec/models/status_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -require "rails_helper" - -describe Status do - describe "colors_as_booleans" do - it "shows the red light as a boolean if the last status is red" do - FactoryBot.create :status, red: true - colors = Status.colors_as_booleans - expect(colors[:red]).to eq(true) - end - end - - describe "colors" do - describe "without a username" do - it "shows the red light on if the last status is red" do - FactoryBot.create :status, red: true - colors = Status.colors - expect(colors[:red]).to be_truthy - end - - it "shows the red light as a count if the last status is red" do - FactoryBot.create :status, red: true - colors = Status.colors - expect(colors[:red]).to eq(1) - end - - it "shows the red and yellow lights on if the last status is yellow, but previous non-yellow was red" do - FactoryBot.create :status, red: true - 2.times { FactoryBot.create :status, red: false, yellow: true } - colors = Status.colors - expect(colors[:yellow]).to be(true) - expect(colors[:red]).to be_truthy - end - - it "shows the red light on if the last status is green, but another project is red" do - FactoryBot.create :status, red: true - FactoryBot.create :status, red: false - colors = Status.colors - expect(colors[:red]).to be_truthy - end - end - - describe "with a username" do - before do - FactoryBot.create :status, username: "danielmorrison", red: true, yellow: true - end - - it "shows the red light on if the last status is red" do - FactoryBot.create :status, username: "collectiveidea", red: true - colors = Status.colors("collectiveidea") - expect(colors[:red]).to be_truthy - end - - it "shows the red and yellow lights on if the last status is yellow, but previous non-yellow was red" do - FactoryBot.create :status, username: "collectiveidea", red: true - 2.times { FactoryBot.create :status, username: "collectiveidea", red: false, yellow: true } - colors = Status.colors("collectiveidea") - expect(colors[:yellow]).to be(true) - expect(colors[:red]).to be_truthy - end - - it "shows the red light on if the last status is green, but another project is red" do - FactoryBot.create :status, username: "collectiveidea", red: true - FactoryBot.create :status, username: "collectiveidea", red: false - colors = Status.colors("collectiveidea") - expect(colors[:red]).to be_truthy - end - end - - describe "with multiple usernames" do - it "shows the red light on if the last status is red" do - FactoryBot.create :status, username: "collectiveidea", red: true, yellow: false - FactoryBot.create :status, username: "danielmorrison", red: false, yellow: true - colors = Status.colors(["collectiveidea", "danielmorrison"]) - expect(colors[:red]).to be_truthy - expect(colors[:yellow]).to be(true) - end - end - end - - describe "#name" do - it "returns the full github-style name" do - status = Status.new(username: "collectiveidea", project_name: "foo") - expect(status.name).to eq("collectiveidea/foo") - end - end - - describe "#devices" do - it "returns the list of Device objects that care about this status" do - device1 = FactoryBot.create(:device, usernames: ["collectiveidea"], projects: ["deadmanssnitch/foo"]) - device2 = FactoryBot.create(:device, usernames: ["collectiveidea", "deadmanssnitch"]) - device3 = FactoryBot.create(:device, usernames: ["deadmanssnitch"], projects: ["collectiveidea/foo"]) - device4 = FactoryBot.create(:device, usernames: [], projects: ["collectiveidea/foo"]) - device5 = FactoryBot.create(:device, usernames: ["deadmanssnitch"], projects: []) - - status = FactoryBot.create(:status, username: "collectiveidea", project_name: "foo") - - expect(status.devices).to include(device1) - expect(status.devices).to include(device2) - expect(status.devices).to include(device3) - expect(status.devices).to include(device4) - expect(status.devices).to include(device4) - expect(status.devices).not_to include(device5) - end - end -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb deleted file mode 100644 index eae2a1f..0000000 --- a/spec/rails_helper.rb +++ /dev/null @@ -1,50 +0,0 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= "test" -require "spec_helper" -require File.expand_path("../../config/environment", __FILE__) -require "rspec/rails" -# Add additional requires below this line. Rails is not loaded until this point! - -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -Rails.root.glob("spec/support/**/*.rb").sort.each { |f| require f } - -# Checks for pending migrations before tests are run. -# If you are not using ActiveRecord, you can remove this line. -ActiveRecord::Migration.maintain_test_schema! - -RSpec.configure do |config| - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_paths = [Rails.root.join("spec/fixtures")] - - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = true - - # RSpec Rails can automatically mix in different behaviours to your tests - # based on their file location, for example enabling you to call `get` and - # `post` in specs under `spec/controllers`. - # - # You can disable this behaviour by removing the line below, and instead - # explicitly tag your specs with their type, e.g.: - # - # RSpec.describe UsersController, :type => :controller do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs - config.infer_spec_type_from_file_location! -end diff --git a/spec/requests/circle_webhooks_spec.rb b/spec/requests/circle_webhooks_spec.rb deleted file mode 100644 index 8bb632a..0000000 --- a/spec/requests/circle_webhooks_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require "rails_helper" - -describe "Webhooks from Circle CI" do - before do - allow(Particle).to receive(:publish) - end - - it "recieves a json payload" do - post "/", params: json_fixture("circle.json"), headers: {"content-type": "application/json", "Circleci-Event-Type": "workflow-completed"} - expect(response).to be_successful - end - - it "saves useful data" do - post "/", params: json_fixture("circle.json"), headers: {"content-type": "application/json", "Circleci-Event-Type": "workflow-completed"} - status = Status.order("created_at DESC").first - expect(status.red).to be(false) - expect(status.service).to eq("circle") - expect(status.project_id).to be_nil - expect(status.project_name).to eq("buildlight") - expect(status.username).to eq("collectiveidea") - end - - it "ignores pull requests" do - expect(Status.count).to eq(0) - post "/", params: json_fixture("circle_pr.json"), headers: {"content-type": "application/json", "Circleci-Event-Type": "workflow-completed"} - expect(Status.count).to eq(0) - end - - it "notifies Particle" do - FactoryBot.create(:device, :with_identifier, usernames: ["collectiveidea"]) - allow(Particle).to receive(:publish) - post "/", params: json_fixture("circle.json"), headers: {"content-type": "application/json", "Circleci-Event-Type": "workflow-completed"} - expect(Particle).to have_received(:publish).with(name: "build_state", data: "passing", ttl: 3600, private: false) - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 976d0de..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,85 +0,0 @@ -# This file was generated by the `rails generate rspec:install` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# The `.rspec` file also contains a few flags that are not defaults but that -# users commonly want. -# -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - # # These two settings work together to allow you to limit a spec run - # # to individual examples or groups you care about by tagging them with - # # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # # get run. - # config.filter_run :focus - # config.run_all_when_everything_filtered = true - # - # # Limits the available syntax to the non-monkey patched syntax that is - # # recommended. For more details, see: - # # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax - # # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching - # config.disable_monkey_patching! - # - # # Many RSpec users commonly either run the entire suite or an individual - # # file, and it's useful to allow more verbose output when running an - # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = 'doc' - # end - # - # # Print the 10 slowest examples and example groups at the - # # end of the spec run, to help surface which specs are running - # # particularly slow. - # config.profile_examples = 10 - # - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -end diff --git a/spec/support/json_helpers.rb b/spec/support/json_helpers.rb deleted file mode 100644 index 0c37f2d..0000000 --- a/spec/support/json_helpers.rb +++ /dev/null @@ -1,11 +0,0 @@ -module JSONHelpers - ENV = {"CONTENT_TYPE" => "application/json"}.freeze - - def json_fixture(filename) - Rails.root.join("spec", "fixtures", filename).read - end -end - -RSpec.configure do |config| - config.include JSONHelpers -end diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..bf5a744 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,123 @@ +use std::sync::{Arc, RwLock}; + +use axum::Router; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::{get, post}; +use sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; +use tera::Tera; +use tokio::sync::broadcast; + +use crate::config::Config; +use crate::handlers; +use crate::ws; + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, + pub tera: Arc>, + pub config: Config, + pub broadcaster: broadcast::Sender, + pub http_client: reqwest::Client, +} + +pub async fn create_app(config: Config) -> Router { + let pool = PgPoolOptions::new() + .max_connections(15) + .connect(&config.database_url) + .await + .expect("Failed to connect to database"); + + create_app_with_pool(config, pool).await +} + +pub async fn create_app_with_pool(config: Config, pool: PgPool) -> Router { + sqlx::migrate!("./migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + build_router(config, pool) +} + +/// Build the router without running migrations. Used in tests where +/// sqlx::test has already applied migrations. +pub fn build_router(config: Config, pool: PgPool) -> Router { + // Load templates + let template_glob = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*.html"); + let tera = Tera::new(template_glob).expect("Failed to load templates"); + + let (tx, _rx) = broadcast::channel::(256); + + let state = AppState { + pool, + tera: Arc::new(RwLock::new(tera)), + config, + broadcaster: tx, + http_client: reqwest::Client::new(), + }; + + Router::new() + // Health check + .route("/up", get(handlers::health::health_check)) + // WebSocket + .route("/ws", get(ws::ws_handler)) + // API routes + .route("/api/devices/{id}", get(handlers::api::devices::show)) + .route("/api/device/trigger", post(handlers::api::devices::trigger)) + .route("/api/device/{id}/red", get(handlers::api::red::show)) + .route("/api/device/{id}/red.json", get(handlers::api::red::show_json)) + // Device routes + .route("/devices/{id}", get(handlers::devices::show)) + // Root: GET = colors index, POST = webhook ingestion + .route("/", get(handlers::colors::index).post(handlers::webhooks::create)) + // Fallback: try public/ static files, then treat as colors show + .fallback(fallback_handler) + .with_state(state) +} + +/// Handles all unmatched routes: serves static files from public/ or +/// treats the path as a colors show request (e.g., /collectiveidea.json). +async fn fallback_handler( + state: axum::extract::State, + req: axum::extract::Request, +) -> axum::response::Response { + let path = req.uri().path().to_string(); + + // Try to serve from public/ + let file_path = format!("{}/public{}", env!("CARGO_MANIFEST_DIR"), path); + if let Ok(contents) = tokio::fs::read(&file_path).await { + let content_type = match std::path::Path::new(&path) + .extension() + .and_then(|e| e.to_str()) + { + Some("ico") => "image/x-icon", + Some("png") => "image/png", + Some("svg") => "image/svg+xml", + Some("gif") => "image/gif", + Some("html") => "text/html; charset=utf-8", + Some("txt") => "text/plain", + Some("css") => "text/css", + Some("js") => "application/javascript", + _ => "application/octet-stream", + }; + return ( + StatusCode::OK, + [(axum::http::header::CONTENT_TYPE, content_type)], + contents, + ) + .into_response(); + } + + // Otherwise treat as colors show (e.g., /collectiveidea or /collectiveidea.json) + let ids = path.trim_start_matches('/').to_string(); + if !ids.is_empty() { + match handlers::colors::show(state.0, ids).await { + Ok(response) => return response, + Err(e) => return e.into_response(), + } + } + + StatusCode::NOT_FOUND.into_response() +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e0f77dd --- /dev/null +++ b/src/config.rs @@ -0,0 +1,24 @@ +#[derive(Clone, Debug)] +pub struct Config { + pub database_url: String, + pub port: u16, + pub host: String, + pub particle_access_token: Option, +} + +impl Config { + pub fn from_env() -> Self { + dotenvy::dotenv().ok(); + + Self { + database_url: std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://localhost/buildlight_development".into()), + port: std::env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(3001), + host: std::env::var("HOST").unwrap_or_else(|_| "localhost:3001".into()), + particle_access_token: std::env::var("PARTICLE_ACCESS_TOKEN").ok().filter(|s| !s.is_empty()), + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..052a3f1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,53 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +#[derive(Debug)] +pub enum AppError { + NotFound, + BadRequest, + Internal(String), + Database(sqlx::Error), + Template(tera::Error), +} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppError::NotFound => write!(f, "Not found"), + AppError::BadRequest => write!(f, "Bad request"), + AppError::Internal(msg) => write!(f, "Internal error: {msg}"), + AppError::Database(e) => write!(f, "Database error: {e}"), + AppError::Template(e) => write!(f, "Template error: {e}"), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let status = match &self { + AppError::NotFound => StatusCode::NOT_FOUND, + AppError::BadRequest => StatusCode::BAD_REQUEST, + AppError::Internal(_) | AppError::Database(_) | AppError::Template(_) => { + tracing::error!("{self}"); + StatusCode::INTERNAL_SERVER_ERROR + } + }; + + status.into_response() + } +} + +impl From for AppError { + fn from(e: sqlx::Error) -> Self { + match e { + sqlx::Error::RowNotFound => AppError::NotFound, + other => AppError::Database(other), + } + } +} + +impl From for AppError { + fn from(e: tera::Error) -> Self { + AppError::Template(e) + } +} diff --git a/src/handlers/api/devices.rs b/src/handlers/api/devices.rs new file mode 100644 index 0000000..4de1e1f --- /dev/null +++ b/src/handlers/api/devices.rs @@ -0,0 +1,69 @@ +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::Json; +use serde::Deserialize; + +use crate::app::AppState; +use crate::error::AppError; +use crate::models::device::Device; +use crate::services::{trigger_particle, trigger_webhook}; + +#[derive(serde::Serialize)] +pub struct DeviceResponse { + colors: crate::models::status::ColorsAsBooleans, + ryg: String, +} + +/// GET /api/devices/:id - Show device colors and RYG as JSON. +pub async fn show( + State(state): State, + Path(id): Path, +) -> Result, AppError> { + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(id) + .fetch_one(&state.pool) + .await?; + + let colors = device.colors_as_booleans(&state.pool).await?; + let ryg = device.ryg(&state.pool).await?; + + Ok(Json(DeviceResponse { colors, ryg })) +} + +#[derive(Deserialize)] +pub struct TriggerParams { + pub coreid: Option, +} + +/// POST /api/device/trigger - Trigger a device (from Particle). +pub async fn trigger( + State(state): State, + Json(params): Json, +) -> StatusCode { + let Some(ref coreid) = params.coreid else { + return StatusCode::OK; + }; + + let device = match Device::find_by_identifier(&state.pool, coreid).await { + Ok(Some(d)) => d, + _ => return StatusCode::OK, + }; + + // Trigger webhooks and particle + if device.webhook_url.is_some() { + if let (Ok(colors), Ok(ryg)) = ( + device.colors_as_booleans(&state.pool).await, + device.ryg(&state.pool).await, + ) { + trigger_webhook::call(&state.http_client, &state.config.host, &device, &colors, &ryg).await; + } + } + + if device.identifier.is_some() { + if let Some(ref token) = state.config.particle_access_token { + trigger_particle::call(&state.http_client, token, &device).await; + } + } + + StatusCode::OK +} diff --git a/src/handlers/api/mod.rs b/src/handlers/api/mod.rs new file mode 100644 index 0000000..708479a --- /dev/null +++ b/src/handlers/api/mod.rs @@ -0,0 +1,2 @@ +pub mod devices; +pub mod red; diff --git a/src/handlers/api/red.rs b/src/handlers/api/red.rs new file mode 100644 index 0000000..3634291 --- /dev/null +++ b/src/handlers/api/red.rs @@ -0,0 +1,83 @@ +use axum::extract::{Path, State}; +use axum::response::{Html, IntoResponse, Response}; +use axum::Json; + +use crate::app::AppState; +use crate::error::AppError; +use crate::models::device::Device; +use crate::models::status::Status; + +#[derive(serde::Serialize)] +struct RedProject { + username: Option, + project_name: Option, +} + +/// GET /api/device/:id/red - Show failing projects (HTML). +pub async fn show( + State(state): State, + Path(id): Path, +) -> Result { + render_red(&state, &id, "html").await +} + +/// GET /api/device/:id/red.json - Show failing projects (JSON). +pub async fn show_json( + State(state): State, + Path(id): Path, +) -> Result { + render_red(&state, &id, "json").await +} + +async fn render_red(state: &AppState, id: &str, format: &str) -> Result { + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE identifier = $1") + .bind(id) + .fetch_one(&state.pool) + .await?; + + let statuses = device.statuses(&state.pool).await?; + let red_projects: Vec<&Status> = statuses.iter().filter(|s| s.red == Some(true)).collect(); + + match format { + "json" => { + let json: Vec = red_projects + .iter() + .map(|s| RedProject { + username: s.username.clone(), + project_name: s.project_name.clone(), + }) + .collect(); + Ok(Json(json).into_response()) + } + _ => { + let mut context = tera::Context::new(); + let projects: Vec = red_projects + .iter() + .map(|s| { + serde_json::json!({ + "project_name": s.project_name, + "username": s.username, + }) + }) + .collect(); + context.insert("red_projects", &projects); + + let tera = state.tera.read().map_err(|e| AppError::Internal(e.to_string()))?; + + #[cfg(debug_assertions)] + { + drop(tera); + let mut tera = state.tera.write().map_err(|e| AppError::Internal(e.to_string()))?; + let _ = tera.full_reload(); + let html = tera.render("api/red.html", &context)?; + Ok(Html(html).into_response()) + } + + #[cfg(not(debug_assertions))] + { + let html = tera.render("api/red.html", &context)?; + Ok(Html(html).into_response()) + } + } + } +} diff --git a/src/handlers/colors.rs b/src/handlers/colors.rs new file mode 100644 index 0000000..3d0513b --- /dev/null +++ b/src/handlers/colors.rs @@ -0,0 +1,93 @@ +use axum::extract::State; +use axum::http::header; +use axum::response::{Html, IntoResponse, Response}; +use axum::Json; + +use crate::app::AppState; +use crate::error::AppError; +use crate::models::status::Status; + +/// GET / - Show colors page or JSON for all projects. +pub async fn index(State(state): State) -> Result { + render_colors(&state, None, "html").await +} + +/// Called from the fallback handler for paths like /collectiveidea or /collectiveidea.json +pub async fn show(state: AppState, raw: String) -> Result { + // Parse format suffix + let (id_part, format) = if let Some(stripped) = raw.strip_suffix(".json") { + (stripped.to_string(), "json") + } else if let Some(stripped) = raw.strip_suffix(".ryg") { + (stripped.to_string(), "ryg") + } else { + (raw, "html") + }; + + let usernames = if id_part.is_empty() { + None + } else { + Some(id_part.split(',').map(String::from).collect()) + }; + render_colors(&state, usernames, format).await +} + +async fn render_colors(state: &AppState, usernames: Option>, format: &str) -> Result { + match format { + "json" => { + let colors = Status::colors(&state.pool, usernames.as_deref()).await?; + Ok(Json(colors).into_response()) + } + "ryg" => { + let pool = state.pool.clone(); + let usernames_clone = usernames.clone(); + + let stream = async_stream::stream! { + loop { + match Status::ryg(&pool, usernames_clone.as_deref()).await { + Ok(ryg) => yield Ok::<_, std::io::Error>(ryg), + Err(_) => break, + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }; + + let body = axum::body::Body::from_stream(stream); + Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/ryg") + .body(body) + .unwrap() + .into_response()) + } + _ => { + let colors = Status::colors(&state.pool, usernames.as_deref()).await?; + let html = render_colors_page(state, &colors)?; + Ok(Html(html).into_response()) + } + } +} + +fn render_colors_page( + state: &AppState, + colors: &crate::models::status::Colors, +) -> Result { + let mut context = tera::Context::new(); + context.insert("colors", colors); + + let red_count = colors.red.as_i64().unwrap_or(0); + context.insert("red_count", &red_count); + + let tera = state.tera.read().map_err(|e| AppError::Internal(e.to_string()))?; + + #[cfg(debug_assertions)] + { + drop(tera); + let mut tera = state.tera.write().map_err(|e| AppError::Internal(e.to_string()))?; + let _ = tera.full_reload(); + Ok(tera.render("colors/index.html", &context)?) + } + + #[cfg(not(debug_assertions))] + { + Ok(tera.render("colors/index.html", &context)?) + } +} diff --git a/src/handlers/devices.rs b/src/handlers/devices.rs new file mode 100644 index 0000000..143471a --- /dev/null +++ b/src/handlers/devices.rs @@ -0,0 +1,49 @@ +use axum::extract::{Path, State}; +use axum::response::{Html, IntoResponse, Response}; +use axum::Json; + +use crate::app::AppState; +use crate::error::AppError; +use crate::models::device::Device; + +/// GET /devices/:id - Show device colors as HTML or JSON. +pub async fn show( + State(state): State, + Path(slug_or_id): Path, +) -> Result { + let (id_part, format) = if let Some(stripped) = slug_or_id.strip_suffix(".json") { + (stripped.to_string(), "json") + } else { + (slug_or_id, "html") + }; + + let device = Device::find_by_slug_or_id(&state.pool, &id_part).await?; + let colors = device.colors(&state.pool).await?; + + match format { + "json" => Ok(Json(colors).into_response()), + _ => { + let mut context = tera::Context::new(); + context.insert("colors", &colors); + let red_count = colors.red.as_i64().unwrap_or(0); + context.insert("red_count", &red_count); + + let tera = state.tera.read().map_err(|e| AppError::Internal(e.to_string()))?; + + #[cfg(debug_assertions)] + { + drop(tera); + let mut tera = state.tera.write().map_err(|e| AppError::Internal(e.to_string()))?; + let _ = tera.full_reload(); + let html = tera.render("colors/index.html", &context)?; + Ok(Html(html).into_response()) + } + + #[cfg(not(debug_assertions))] + { + let html = tera.render("colors/index.html", &context)?; + Ok(Html(html).into_response()) + } + } + } +} diff --git a/src/handlers/health.rs b/src/handlers/health.rs new file mode 100644 index 0000000..64fd1f8 --- /dev/null +++ b/src/handlers/health.rs @@ -0,0 +1,5 @@ +use axum::http::StatusCode; + +pub async fn health_check() -> StatusCode { + StatusCode::OK +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..79688b9 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod colors; +pub mod devices; +pub mod health; +pub mod webhooks; diff --git a/src/handlers/webhooks.rs b/src/handlers/webhooks.rs new file mode 100644 index 0000000..ced5d47 --- /dev/null +++ b/src/handlers/webhooks.rs @@ -0,0 +1,144 @@ +use axum::extract::State; +use axum::http::{HeaderMap, StatusCode}; +use axum::Json; + +use crate::app::AppState; +use crate::models::status::Status; +use crate::services::{parse_circle, parse_github, parse_travis, trigger_particle, trigger_webhook}; + +/// POST / - Receive webhooks from CI services. +pub async fn create( + State(state): State, + headers: HeaderMap, + Json(body): Json, +) -> StatusCode { + // Travis sends the payload as a form-encoded "payload" string field. + // Our Axum handler receives JSON, but Travis payloads come as a string. + if let Some(payload_str) = body.get("payload").and_then(|v| v.as_str()) { + match parse_travis::call(&state.pool, payload_str).await { + Ok(Some(status)) => { + broadcast_and_trigger(&state, &status).await; + return StatusCode::OK; + } + Ok(None) => return StatusCode::OK, + Err(e) => { + tracing::error!("Travis parse error: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR; + } + } + } + + // GitHub Actions: has "repository" containing "/" but no "payload" + if body.get("payload").is_none() { + if let Some(repo) = body.get("repository").and_then(|v| v.as_str()) { + if repo.contains('/') { + let payload = parse_github::GitHubPayload { + status: body + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + repository: repo.to_string(), + workflow: body.get("workflow").and_then(|v| v.as_str()).map(String::from), + }; + + match parse_github::call(&state.pool, &payload).await { + Ok(status) => { + broadcast_and_trigger(&state, &status).await; + return StatusCode::OK; + } + Err(e) => { + tracing::error!("GitHub parse error: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR; + } + } + } + } + } + + // CircleCI: has Circleci-Event-Type header + let is_circle = headers + .get("circleci-event-type") + .is_some(); + + if is_circle { + let payload: parse_circle::CirclePayload = match serde_json::from_value(body) { + Ok(p) => p, + Err(e) => { + tracing::error!("CircleCI parse error: {e}"); + return StatusCode::BAD_REQUEST; + } + }; + + match parse_circle::call(&state.pool, &payload).await { + Ok(Some(status)) => { + broadcast_and_trigger(&state, &status).await; + return StatusCode::OK; + } + Ok(None) => return StatusCode::OK, + Err(e) => { + tracing::error!("CircleCI error: {e}"); + return StatusCode::INTERNAL_SERVER_ERROR; + } + } + } + + StatusCode::BAD_REQUEST +} + +async fn broadcast_and_trigger(state: &AppState, status: &Status) { + // Broadcast to colors channels + if let Ok(colors) = Status::colors(&state.pool, None).await { + let _ = state.broadcaster.send(crate::ws::BroadcastMsg { + channel: "colors".into(), + id: "*".into(), + payload: serde_json::to_value(&colors).unwrap_or_default(), + }); + } + + if let Some(ref username) = status.username { + if let Ok(colors) = Status::colors(&state.pool, Some(&[username.clone()])).await { + let _ = state.broadcaster.send(crate::ws::BroadcastMsg { + channel: "colors".into(), + id: username.clone(), + payload: serde_json::to_value(&colors).unwrap_or_default(), + }); + } + } + + // Update devices watching this status + if let Ok(devices) = status.devices(&state.pool).await { + for mut device in devices { + let changed = device.update_status(&state.pool).await.unwrap_or(false); + + // Broadcast to device channel + if let Some(ref slug) = device.slug { + if let Ok(colors) = device.colors(&state.pool).await { + let _ = state.broadcaster.send(crate::ws::BroadcastMsg { + channel: "device".into(), + id: slug.clone(), + payload: serde_json::to_value(&colors).unwrap_or_default(), + }); + } + } + + // Only trigger external actions on status change + if changed { + if device.webhook_url.is_some() { + if let (Ok(colors), Ok(ryg)) = + (device.colors_as_booleans(&state.pool).await, device.ryg(&state.pool).await) + { + trigger_webhook::call(&state.http_client, &state.config.host, &device, &colors, &ryg) + .await; + } + } + + if device.identifier.is_some() { + if let Some(ref token) = state.config.particle_access_token { + trigger_particle::call(&state.http_client, token, &device).await; + } + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..23017aa --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod app; +pub mod config; +pub mod error; +pub mod handlers; +pub mod models; +pub mod services; +pub mod ws; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bce1b59 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +use buildlight::app; +use buildlight::config::Config; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) + .init(); + + let config = Config::from_env(); + let addr = format!("0.0.0.0:{}", config.port); + + tracing::info!("Starting BuildLight on {}", addr); + + let app = app::create_app(config).await; + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/src/models/device.rs b/src/models/device.rs new file mode 100644 index 0000000..83ca5db --- /dev/null +++ b/src/models/device.rs @@ -0,0 +1,172 @@ +use chrono::NaiveDateTime; +use sqlx::PgPool; +use uuid::Uuid; + +use super::status::{Colors, ColorsAsBooleans, Status}; + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct Device { + pub id: Uuid, + pub usernames: Vec, + pub projects: Vec, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub identifier: Option, + pub name: String, + pub webhook_url: Option, + pub slug: Option, + pub status: Option, + pub status_changed_at: Option, +} + +impl Device { + pub async fn find_by_slug_or_id(pool: &PgPool, slug_or_id: &str) -> Result { + // Try UUID first, then slug + if let Ok(uuid) = Uuid::parse_str(slug_or_id) { + let device = sqlx::query_as::<_, Device>( + "SELECT * FROM devices WHERE id = $1 OR slug = $2 LIMIT 1", + ) + .bind(uuid) + .bind(slug_or_id) + .fetch_one(pool) + .await?; + return Ok(device); + } + + sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE slug = $1") + .bind(slug_or_id) + .fetch_one(pool) + .await + } + + pub async fn find_by_identifier(pool: &PgPool, identifier: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE identifier = $1") + .bind(identifier) + .fetch_optional(pool) + .await + } + + /// Get all statuses that this device is watching. + pub async fn statuses(&self, pool: &PgPool) -> Result, sqlx::Error> { + sqlx::query_as::<_, Status>( + "SELECT * FROM statuses WHERE username = ANY($1) \ + OR (username || '/' || project_name) = ANY($2)", + ) + .bind(&self.usernames) + .bind(&self.projects) + .fetch_all(pool) + .await + } + + /// Get colors for this device based on its watched statuses. + pub async fn colors(&self, pool: &PgPool) -> Result { + let red_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM statuses \ + WHERE red = true AND (username = ANY($1) OR (username || '/' || project_name) = ANY($2))", + ) + .bind(&self.usernames) + .bind(&self.projects) + .fetch_one(pool) + .await?; + + let yellow: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses \ + WHERE yellow = true AND (username = ANY($1) OR (username || '/' || project_name) = ANY($2)))", + ) + .bind(&self.usernames) + .bind(&self.projects) + .fetch_one(pool) + .await?; + + let red_value = if red_count > 0 { + serde_json::Value::Number(red_count.into()) + } else { + serde_json::Value::Bool(false) + }; + + Ok(Colors { + green: red_count == 0, + red: red_value, + yellow, + }) + } + + pub async fn colors_as_booleans(&self, pool: &PgPool) -> Result { + let colors = self.colors(pool).await?; + Ok(ColorsAsBooleans { + red: colors.red.as_bool().map_or(true, |b| b), + yellow: colors.yellow, + green: colors.green, + }) + } + + pub async fn ryg(&self, pool: &PgPool) -> Result { + let colors = self.colors(pool).await?; + let r = if colors.red != serde_json::Value::Bool(false) { 'R' } else { 'r' }; + let y = if colors.yellow { 'Y' } else { 'y' }; + let g = if colors.green { 'G' } else { 'g' }; + Ok(format!("{r}{y}{g}")) + } + + /// Compute and return the current status string (e.g. "passing", "failing-building"). + pub async fn current_status(&self, pool: &PgPool) -> Result { + let has_red: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses \ + WHERE red = true AND (username = ANY($1) OR (username || '/' || project_name) = ANY($2)))", + ) + .bind(&self.usernames) + .bind(&self.projects) + .fetch_one(pool) + .await?; + + let has_yellow: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses \ + WHERE yellow = true AND (username = ANY($1) OR (username || '/' || project_name) = ANY($2)))", + ) + .bind(&self.usernames) + .bind(&self.projects) + .fetch_one(pool) + .await?; + + let mut parts = Vec::new(); + if !has_red { + parts.push("passing"); + } + if has_red { + parts.push("failing"); + } + if has_yellow { + parts.push("building"); + } + Ok(parts.join("-")) + } + + /// Update the device status and persist if changed. Returns whether the status changed. + pub async fn update_status(&mut self, pool: &PgPool) -> Result { + let new_status = self.current_status(pool).await?; + let changed = self.status.as_deref() != Some(&new_status); + + self.status = Some(new_status); + + if changed { + self.status_changed_at = Some(chrono::Utc::now().naive_utc()); + sqlx::query("UPDATE devices SET status = $1, status_changed_at = $2, updated_at = NOW() WHERE id = $3") + .bind(&self.status) + .bind(self.status_changed_at) + .bind(self.id) + .execute(pool) + .await?; + } + + Ok(changed) + } + + /// Helper to get colors by slug (for WebSocket initial state). + pub async fn colors_by_slug(pool: &PgPool, slug: &str) -> Result { + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE slug = $1") + .bind(slug) + .fetch_one(pool) + .await?; + device.colors(pool).await + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..1c0d91a --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod device; +pub mod status; diff --git a/src/models/status.rs b/src/models/status.rs new file mode 100644 index 0000000..8107166 --- /dev/null +++ b/src/models/status.rs @@ -0,0 +1,267 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use sqlx::PgPool; + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct Status { + pub id: i64, + pub project_id: Option, + pub project_name: Option, + pub created_at: Option, + pub updated_at: Option, + pub payload: Option, + pub red: Option, + pub yellow: Option, + pub username: Option, + pub service: String, + pub workflow: Option, +} + +/// Colors representation where `red` is either a count (positive i64) or false. +#[derive(Debug, Clone, Serialize)] +pub struct Colors { + pub red: serde_json::Value, + pub yellow: bool, + pub green: bool, +} + +/// Colors with all boolean values. +#[derive(Debug, Clone, Serialize)] +pub struct ColorsAsBooleans { + pub red: bool, + pub yellow: bool, + pub green: bool, +} + +impl Status { + pub fn name(&self) -> String { + format!( + "{}/{}", + self.username.as_deref().unwrap_or(""), + self.project_name.as_deref().unwrap_or("") + ) + } + + /// Query the aggregate colors for all statuses, optionally filtered by username(s). + pub async fn colors(pool: &PgPool, usernames: Option<&[String]>) -> Result { + let red_count: i64 = match usernames { + Some(names) if !names.is_empty() => { + sqlx::query_scalar("SELECT COUNT(*) FROM statuses WHERE red = true AND username = ANY($1)") + .bind(names) + .fetch_one(pool) + .await? + } + _ => { + sqlx::query_scalar("SELECT COUNT(*) FROM statuses WHERE red = true") + .fetch_one(pool) + .await? + } + }; + + let yellow: bool = match usernames { + Some(names) if !names.is_empty() => { + sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses WHERE yellow = true AND username = ANY($1))", + ) + .bind(names) + .fetch_one(pool) + .await? + } + _ => { + sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM statuses WHERE yellow = true)") + .fetch_one(pool) + .await? + } + }; + + let red_value = if red_count > 0 { + serde_json::Value::Number(red_count.into()) + } else { + serde_json::Value::Bool(false) + }; + + Ok(Colors { + green: red_count == 0, + red: red_value, + yellow, + }) + } + + pub async fn colors_as_booleans( + pool: &PgPool, + usernames: Option<&[String]>, + ) -> Result { + let colors = Self::colors(pool, usernames).await?; + Ok(ColorsAsBooleans { + red: colors.red.as_bool().map_or(true, |b| b), // number means truthy + yellow: colors.yellow, + green: colors.green, + }) + } + + /// Returns "RYG" string where uppercase means on, lowercase means off. + pub async fn ryg(pool: &PgPool, usernames: Option<&[String]>) -> Result { + let colors = Self::colors(pool, usernames).await?; + let r = if colors.red != serde_json::Value::Bool(false) { 'R' } else { 'r' }; + let y = if colors.yellow { 'Y' } else { 'y' }; + let g = if colors.green { 'G' } else { 'g' }; + Ok(format!("{r}{y}{g}")) + } + + /// Returns the combined status string like "passing", "failing-building", etc. + pub async fn current_status(pool: &PgPool, usernames: Option<&[String]>) -> Result { + let has_red: bool = match usernames { + Some(names) if !names.is_empty() => { + sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses WHERE red = true AND username = ANY($1))", + ) + .bind(names) + .fetch_one(pool) + .await? + } + _ => { + sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM statuses WHERE red = true)") + .fetch_one(pool) + .await? + } + }; + + let has_yellow: bool = match usernames { + Some(names) if !names.is_empty() => { + sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM statuses WHERE yellow = true AND username = ANY($1))", + ) + .bind(names) + .fetch_one(pool) + .await? + } + _ => { + sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM statuses WHERE yellow = true)") + .fetch_one(pool) + .await? + } + }; + + let mut parts = Vec::new(); + if !has_red { + parts.push("passing"); + } + if has_red { + parts.push("failing"); + } + if has_yellow { + parts.push("building"); + } + Ok(parts.join("-")) + } + + /// Find devices that are watching this status. + pub async fn devices(&self, pool: &PgPool) -> Result, sqlx::Error> { + let name = self.name(); + let username = self.username.as_deref().unwrap_or(""); + + sqlx::query_as::<_, super::device::Device>( + "SELECT * FROM devices WHERE usernames @> ARRAY[$1]::varchar[] OR projects @> ARRAY[$2]::varchar[]", + ) + .bind(username) + .bind(&name) + .fetch_all(pool) + .await + } + + /// Upsert a status by service + username + project_name + workflow. + pub async fn upsert( + pool: &PgPool, + service: &str, + project_id: Option<&str>, + username: Option<&str>, + project_name: Option<&str>, + workflow: Option<&str>, + red: Option, + yellow: Option, + ) -> Result { + // Try to find existing + let existing = sqlx::query_as::<_, Status>( + "SELECT * FROM statuses WHERE service = $1 AND \ + (username IS NOT DISTINCT FROM $2) AND \ + (project_name IS NOT DISTINCT FROM $3) AND \ + (workflow IS NOT DISTINCT FROM $4) AND \ + (project_id IS NOT DISTINCT FROM $5)", + ) + .bind(service) + .bind(username) + .bind(project_name) + .bind(workflow) + .bind(project_id) + .fetch_optional(pool) + .await?; + + match existing { + Some(mut status) => { + if let Some(r) = red { + status.red = Some(r); + } + if let Some(y) = yellow { + status.yellow = Some(y); + } + // Update username/project_name if provided (Travis sets these from payload) + if username.is_some() { + status.username = username.map(String::from); + } + if project_name.is_some() { + status.project_name = project_name.map(String::from); + } + + sqlx::query_as::<_, Status>( + "UPDATE statuses SET red = $1, yellow = $2, username = $3, project_name = $4, updated_at = NOW() \ + WHERE id = $5 RETURNING *", + ) + .bind(status.red) + .bind(status.yellow) + .bind(&status.username) + .bind(&status.project_name) + .bind(status.id) + .fetch_one(pool) + .await + } + None => { + sqlx::query_as::<_, Status>( + "INSERT INTO statuses (service, project_id, username, project_name, workflow, red, yellow, created_at, updated_at) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING *", + ) + .bind(service) + .bind(project_id) + .bind(username) + .bind(project_name) + .bind(workflow) + .bind(red) + .bind(yellow) + .fetch_one(pool) + .await + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name() { + let status = Status { + id: 1, + project_id: None, + project_name: Some("foo".into()), + created_at: None, + updated_at: None, + payload: None, + red: Some(false), + yellow: Some(false), + username: Some("collectiveidea".into()), + service: "github".into(), + workflow: None, + }; + assert_eq!(status.name(), "collectiveidea/foo"); + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..5364664 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,5 @@ +pub mod parse_circle; +pub mod parse_github; +pub mod parse_travis; +pub mod trigger_particle; +pub mod trigger_webhook; diff --git a/src/services/parse_circle.rs b/src/services/parse_circle.rs new file mode 100644 index 0000000..2783b49 --- /dev/null +++ b/src/services/parse_circle.rs @@ -0,0 +1,92 @@ +use serde::Deserialize; +use sqlx::PgPool; + +use crate::models::status::Status; + +#[derive(Debug, Deserialize)] +pub struct CirclePayload { + #[serde(rename = "type")] + pub event_type: Option, + pub pipeline: Option, + pub project: Option, + pub organization: Option, + pub workflow: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CirclePipeline { + pub vcs: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CircleVcs { + pub branch: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CircleProject { + pub name: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CircleOrganization { + pub name: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CircleWorkflow { + pub status: Option, +} + +pub async fn call(pool: &PgPool, payload: &CirclePayload) -> Result, sqlx::Error> { + // Only process workflow-completed events + if payload.event_type.as_deref() != Some("workflow-completed") { + return Ok(None); + } + + // Ignore non-main branches (PR builds) + let branch = payload + .pipeline + .as_ref() + .and_then(|p| p.vcs.as_ref()) + .and_then(|v| v.branch.as_deref()); + + if !matches!(branch, Some("main" | "master")) { + return Ok(None); + } + + let username = payload.organization.as_ref().and_then(|o| o.name.as_deref()); + let project_name = payload.project.as_ref().and_then(|p| p.name.as_deref()); + let workflow_status = payload.workflow.as_ref().and_then(|w| w.status.as_deref()); + + let (red, yellow) = set_colors(workflow_status.unwrap_or("unknown")); + + let status = Status::upsert(pool, "circle", None, username, project_name, None, red, yellow).await?; + + Ok(Some(status)) +} + +pub fn set_colors(code: &str) -> (Option, Option) { + // CircleCI has no in-progress state via webhooks, so no yellow + let red = code != "success"; + (Some(red), Some(false)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_colors_success() { + let (red, yellow) = set_colors("success"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_failed() { + let (red, yellow) = set_colors("failed"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); + } +} diff --git a/src/services/parse_github.rs b/src/services/parse_github.rs new file mode 100644 index 0000000..580aa04 --- /dev/null +++ b/src/services/parse_github.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; +use sqlx::PgPool; + +use crate::models::status::Status; + +#[derive(Debug, Deserialize)] +pub struct GitHubPayload { + pub status: String, + pub repository: String, + pub workflow: Option, +} + +pub async fn call(pool: &PgPool, payload: &GitHubPayload) -> Result { + let parts: Vec<&str> = payload.repository.splitn(2, '/').collect(); + let (username, project_name) = if parts.len() == 2 { + (Some(parts[0]), Some(parts[1])) + } else { + (None, None) + }; + + let (red, yellow) = set_colors(payload.status.as_str()); + + Status::upsert( + pool, + "github", + None, + username, + project_name, + payload.workflow.as_deref(), + red, + yellow, + ) + .await +} + +/// Returns (red, yellow) overrides. None means "keep existing value". +pub fn set_colors(code: &str) -> (Option, Option) { + match code { + "" => (None, Some(true)), + "success" => (Some(false), Some(false)), + "failure" => (Some(true), Some(false)), + other => panic!("Unknown status: {other}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_colors_success() { + let (red, yellow) = set_colors("success"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_failure() { + let (red, yellow) = set_colors("failure"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_empty() { + let (red, yellow) = set_colors(""); + assert_eq!(red, None); // keeps existing red value + assert_eq!(yellow, Some(true)); + } +} diff --git a/src/services/parse_travis.rs b/src/services/parse_travis.rs new file mode 100644 index 0000000..7f5fac9 --- /dev/null +++ b/src/services/parse_travis.rs @@ -0,0 +1,90 @@ +use serde::Deserialize; +use sqlx::PgPool; + +use crate::models::status::Status; + +#[derive(Debug, Deserialize)] +pub struct TravisPayload { + pub id: Option, + pub repository: TravisRepository, + pub status_message: String, + #[serde(rename = "type")] + pub event_type: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TravisRepository { + pub id: serde_json::Value, + pub name: String, + pub owner_name: String, +} + +pub async fn call(pool: &PgPool, raw_payload: &str) -> Result, sqlx::Error> { + let payload: TravisPayload = match serde_json::from_str(raw_payload) { + Ok(p) => p, + Err(_) => return Ok(None), + }; + + // Ignore pull requests + if payload.event_type.as_deref() == Some("pull_request") { + return Ok(None); + } + + let (red, yellow) = set_colors(&payload.status_message); + let project_id = payload.repository.id.to_string(); + + let status = Status::upsert( + pool, + "travis", + Some(&project_id), + Some(&payload.repository.owner_name), + Some(&payload.repository.name), + None, + red, + yellow, + ) + .await?; + + Ok(Some(status)) +} + +pub fn set_colors(code: &str) -> (Option, Option) { + match code { + "Pending" => (None, Some(true)), + "Passed" | "Fixed" => (Some(false), Some(false)), + _ => (Some(true), Some(false)), // "Still Failing", "Failed", "Broken", "Errored", etc. + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_colors_passed() { + let (red, yellow) = set_colors("Passed"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_fixed() { + let (red, yellow) = set_colors("Fixed"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_still_failing() { + let (red, yellow) = set_colors("Still Failing"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); + } + + #[test] + fn test_set_colors_pending() { + let (red, yellow) = set_colors("Pending"); + assert_eq!(red, None); + assert_eq!(yellow, Some(true)); + } +} diff --git a/src/services/trigger_particle.rs b/src/services/trigger_particle.rs new file mode 100644 index 0000000..e015473 --- /dev/null +++ b/src/services/trigger_particle.rs @@ -0,0 +1,21 @@ +use crate::models::device::Device; + +pub async fn call(client: &reqwest::Client, access_token: &str, device: &Device) { + let status = device.status.as_deref().unwrap_or("passing"); + + let result = client + .post("https://api.particle.io/v1/events") + .bearer_auth(access_token) + .form(&[ + ("name", "build_state"), + ("data", status), + ("ttl", "3600"), + ("private", "false"), + ]) + .send() + .await; + + if let Err(e) = result { + tracing::error!("Failed to publish to Particle: {e}"); + } +} diff --git a/src/services/trigger_webhook.rs b/src/services/trigger_webhook.rs new file mode 100644 index 0000000..f2f6a58 --- /dev/null +++ b/src/services/trigger_webhook.rs @@ -0,0 +1,27 @@ +use crate::models::device::Device; +use crate::models::status::ColorsAsBooleans; + +pub async fn call(client: &reqwest::Client, host: &str, device: &Device, colors: &ColorsAsBooleans, ryg: &str) { + let Some(ref webhook_url) = device.webhook_url else { + return; + }; + + let body = serde_json::json!({ + "colors": colors, + }); + + let device_url = format!("http://{host}/api/devices/{}", device.id); + + let result = client + .post(webhook_url) + .header("Content-Type", "application/json") + .header("x-ryg", ryg) + .header("x-device-url", device_url) + .body(body.to_string()) + .send() + .await; + + if let Err(e) = result { + tracing::error!("Failed to send webhook to {webhook_url}: {e}"); + } +} diff --git a/src/ws.rs b/src/ws.rs new file mode 100644 index 0000000..9452cfe --- /dev/null +++ b/src/ws.rs @@ -0,0 +1,85 @@ +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::{Query, State, WebSocketUpgrade}; +use axum::response::IntoResponse; +use futures::SinkExt; +use futures::StreamExt; +use serde::Deserialize; + +use crate::app::AppState; + +#[derive(Clone, Debug)] +pub struct BroadcastMsg { + pub channel: String, + pub id: String, + pub payload: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct WsParams { + pub channel: String, + pub id: String, +} + +pub async fn ws_handler( + ws: WebSocketUpgrade, + Query(params): Query, + State(state): State, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, params, state)) +} + +async fn handle_socket(socket: WebSocket, params: WsParams, state: AppState) { + let (mut sender, mut receiver) = socket.split(); + let mut rx = state.broadcaster.subscribe(); + + let channel = params.channel; + let id = params.id; + + // Send initial state + let initial = match channel.as_str() { + "colors" => { + let ids: Option> = if id == "*" { None } else { Some(id.split(',').map(String::from).collect()) }; + crate::models::status::Status::colors(&state.pool, ids.as_deref()).await.ok() + } + "device" => { + let colors = crate::models::device::Device::colors_by_slug(&state.pool, &id).await.ok(); + colors + } + _ => None, + }; + + if let Some(colors) = initial { + let msg = serde_json::json!({"colors": colors}); + if sender.send(Message::Text(msg.to_string().into())).await.is_err() { + return; + } + } + + // Forward broadcast messages matching this subscription + let channel_clone = channel.clone(); + let id_clone = id.clone(); + let send_task = tokio::spawn(async move { + while let Ok(msg) = rx.recv().await { + if msg.channel == channel_clone && (msg.id == id_clone || id_clone == "*" || msg.id == "*") { + let payload = serde_json::json!({"colors": msg.payload}); + if sender.send(Message::Text(payload.to_string().into())).await.is_err() { + break; + } + } + } + }); + + // Drain incoming messages (we don't expect any, but keep the connection alive) + let recv_task = tokio::spawn(async move { + while let Some(Ok(msg)) = receiver.next().await { + if matches!(msg, Message::Close(_)) { + break; + } + } + }); + + tokio::select! { + _ = send_task => {}, + _ = recv_task => {}, + } +} diff --git a/templates/api/red.html b/templates/api/red.html new file mode 100644 index 0000000..cfaca2f --- /dev/null +++ b/templates/api/red.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} + +{% block content %} +
+ {% if red_projects | length > 0 %} +

The following projects are failing

+
    + {% for project in red_projects %} +
  • {{ project.project_name }}
  • + {% endfor %} +
+ {% else %} +
+ 🎉🎉🎉🎉🎉 +
+

You have no failing projects.

+
+ 🎉🎉🎉🎉🎉 +
+ {% endif %} +
+{% endblock %} diff --git a/app/views/colors/index.html.erb b/templates/colors/index.html similarity index 81% rename from app/views/colors/index.html.erb rename to templates/colors/index.html index f75b6f7..6ae59f1 100644 --- a/app/views/colors/index.html.erb +++ b/templates/colors/index.html @@ -1,3 +1,6 @@ +{% extends "layout.html" %} + +{% block content %}

@@ -5,7 +8,7 @@

-

Rats. <%= pluralize @colors[:red], "project is", plural: "projects are" %> failing.

+

Rats. {% if red_count == 1 %}{{ red_count }} project is{% else %}{{ red_count }} projects are{% endif %} failing.

+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..61cecc8 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,24 @@ + + + + + + + [i] Buildlight + {% if colors and colors.red is number %} + + {% elif colors and colors.yellow %} + + {% elif colors %} + + {% else %} + + {% endif %} + + + + + {% block content %}{% endblock %} + + + diff --git a/tests/api_device_tests.rs b/tests/api_device_tests.rs new file mode 100644 index 0000000..aeaac66 --- /dev/null +++ b/tests/api_device_tests.rs @@ -0,0 +1,82 @@ +mod common; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use http_body_util::BodyExt; +use sqlx::PgPool; +use tower::ServiceExt; + +#[sqlx::test(migrations = "./migrations")] +async fn test_api_device_show(pool: PgPool) { + common::create_status(&pool, "travis", Some("test"), None, false, true).await; + let device_id = common::create_device( + &pool, "Test", None, &["test"], &[], None, None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri(format!("/api/devices/{device_id}")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(json["colors"]["red"], false); + assert_eq!(json["colors"]["yellow"], true); + assert_eq!(json["colors"]["green"], true); + assert_eq!(json["ryg"], "rYG"); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_api_device_trigger_with_device(pool: PgPool) { + common::create_device( + &pool, "Test", None, &[], &[], Some("abc123"), None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/device/trigger") + .header("content-type", "application/json") + .body(Body::from( + r#"{"name":"ready","data":"true","coreid":"abc123","published_at":"2016-06-14T22:06:10.976Z"}"#, + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_api_device_trigger_without_device(pool: PgPool) { + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/device/trigger") + .header("content-type", "application/json") + .body(Body::from( + r#"{"name":"ready","data":"true","coreid":"FAKE","published_at":"2016-06-14T22:06:10.976Z"}"#, + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/tests/api_red_tests.rs b/tests/api_red_tests.rs new file mode 100644 index 0000000..64f0e21 --- /dev/null +++ b/tests/api_red_tests.rs @@ -0,0 +1,66 @@ +mod common; + +use axum::body::Body; +use axum::http::Request; +use http_body_util::BodyExt; +use sqlx::PgPool; +use tower::ServiceExt; + +#[sqlx::test(migrations = "./migrations")] +async fn test_red_projects_json(pool: PgPool) { + let red1_id = common::create_status(&pool, "travis", Some("user1"), Some("proj-a"), true, false).await; + common::create_status(&pool, "travis", Some("user2"), Some("proj-b"), true, false).await; + common::create_status(&pool, "travis", Some("user1"), Some("proj-c"), false, false).await; + common::create_status(&pool, "travis", Some("user2"), Some("proj-d"), false, false).await; + + common::create_device( + &pool, "Test", None, &["user1"], &[], Some("abc123"), None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/api/device/abc123/red.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let projects = json.as_array().unwrap(); + + // Only user1's red project should be in the list + assert_eq!(projects.len(), 1); + + // Suppress unused variable warning + let _ = red1_id; +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_red_projects_html(pool: PgPool) { + common::create_status(&pool, "travis", Some("user1"), Some("proj-a"), true, false).await; + common::create_device( + &pool, "Test", None, &["user1"], &[], Some("abc123"), None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/api/device/abc123/red") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let html = String::from_utf8(body.to_vec()).unwrap(); + + assert!(html.contains("proj-a")); +} diff --git a/tests/circle_integration_tests.rs b/tests/circle_integration_tests.rs new file mode 100644 index 0000000..f7c7d82 --- /dev/null +++ b/tests/circle_integration_tests.rs @@ -0,0 +1,88 @@ +mod common; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use http_body_util::BodyExt; +use sqlx::PgPool; +use tower::ServiceExt; + +fn circle_fixture() -> &'static str { + include_str!("fixtures/circle.json") +} + +fn circle_pr_fixture() -> &'static str { + include_str!("fixtures/circle_pr.json") +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_circle_webhook(pool: PgPool) { + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .header("circleci-event-type", "workflow-completed") + .body(Body::from(circle_fixture())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_circle_saves_data(pool: PgPool) { + let app = common::test_app(pool.clone()); + + app.oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .header("circleci-event-type", "workflow-completed") + .body(Body::from(circle_fixture())) + .unwrap(), + ) + .await + .unwrap(); + + let status = sqlx::query_as::<_, buildlight::models::status::Status>( + "SELECT * FROM statuses ORDER BY created_at DESC LIMIT 1", + ) + .fetch_one(&pool) + .await + .unwrap(); + + assert_eq!(status.red, Some(false)); + assert_eq!(status.service, "circle"); + assert_eq!(status.project_id, None); + assert_eq!(status.project_name.as_deref(), Some("buildlight")); + assert_eq!(status.username.as_deref(), Some("collectiveidea")); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_circle_ignores_pull_requests(pool: PgPool) { + let app = common::test_app(pool.clone()); + + app.oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .header("circleci-event-type", "workflow-completed") + .body(Body::from(circle_pr_fixture())) + .unwrap(), + ) + .await + .unwrap(); + + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM statuses") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(count.0, 0); +} diff --git a/tests/colors_tests.rs b/tests/colors_tests.rs new file mode 100644 index 0000000..5a5fe0e --- /dev/null +++ b/tests/colors_tests.rs @@ -0,0 +1,114 @@ +mod common; + +use axum::body::Body; +use axum::http::Request; +use http_body_util::BodyExt; +use sqlx::PgPool; +use tower::ServiceExt; + +#[sqlx::test(migrations = "./migrations")] +async fn test_index_red(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(json["red"].is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_index_red_and_yellow(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + common::create_status(&pool, "travis", None, None, false, true).await; + common::create_status(&pool, "travis", None, None, false, true).await; + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["yellow"], true); + assert!(json["red"].is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_index_another_project_red(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + common::create_status(&pool, "travis", None, None, false, false).await; + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(json["red"].is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_show_single_user(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, false).await; + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, false).await; + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/collectiveidea.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["red"], false); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_show_multiple_users(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, true).await; + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, false).await; + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/collectiveidea,danielmorrison.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert!(json["red"].is_number()); + assert_eq!(json["yellow"], true); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..8e8b92d --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,96 @@ +use axum::Router; +use buildlight::app; +use buildlight::config::Config; +use sqlx::PgPool; + +pub fn test_config() -> Config { + Config { + database_url: String::new(), // Not used; pool is provided directly + port: 3001, + host: "localhost:3001".into(), + particle_access_token: None, + } +} + +pub fn test_app(pool: PgPool) -> Router { + let config = test_config(); + app::build_router(config, pool) +} + +pub async fn create_status( + pool: &PgPool, + service: &str, + username: Option<&str>, + project_name: Option<&str>, + red: bool, + yellow: bool, +) -> i64 { + let row: (i64,) = sqlx::query_as( + "INSERT INTO statuses (service, username, project_name, red, yellow, created_at, updated_at) \ + VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) RETURNING id", + ) + .bind(service) + .bind(username) + .bind(project_name) + .bind(red) + .bind(yellow) + .fetch_one(pool) + .await + .unwrap(); + row.0 +} + +pub async fn create_status_full( + pool: &PgPool, + service: &str, + project_id: Option<&str>, + username: Option<&str>, + project_name: Option<&str>, + workflow: Option<&str>, + red: bool, + yellow: bool, +) -> i64 { + let row: (i64,) = sqlx::query_as( + "INSERT INTO statuses (service, project_id, username, project_name, workflow, red, yellow, created_at, updated_at) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING id", + ) + .bind(service) + .bind(project_id) + .bind(username) + .bind(project_name) + .bind(workflow) + .bind(red) + .bind(yellow) + .fetch_one(pool) + .await + .unwrap(); + row.0 +} + +pub async fn create_device( + pool: &PgPool, + name: &str, + slug: Option<&str>, + usernames: &[&str], + projects: &[&str], + identifier: Option<&str>, + webhook_url: Option<&str>, +) -> uuid::Uuid { + let usernames: Vec = usernames.iter().map(|s| s.to_string()).collect(); + let projects: Vec = projects.iter().map(|s| s.to_string()).collect(); + + let row: (uuid::Uuid,) = sqlx::query_as( + "INSERT INTO devices (name, slug, usernames, projects, identifier, webhook_url, created_at, updated_at) \ + VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING id", + ) + .bind(name) + .bind(slug) + .bind(&usernames) + .bind(&projects) + .bind(identifier) + .bind(webhook_url) + .fetch_one(pool) + .await + .unwrap(); + row.0 +} diff --git a/tests/device_tests.rs b/tests/device_tests.rs new file mode 100644 index 0000000..95ecb63 --- /dev/null +++ b/tests/device_tests.rs @@ -0,0 +1,107 @@ +mod common; + +use buildlight::models::device::Device; +use sqlx::PgPool; + +#[sqlx::test(migrations = "./migrations")] +async fn test_statuses_by_project(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("inchworm"), Some("foo"), false, false).await; + + let device_id = common::create_device( + &pool, "Test Device", None, &[], &["collectiveidea/bar", "deadmanssnitch/foo"], None, None, + ).await; + + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(device_id) + .fetch_one(&pool) + .await + .unwrap(); + + let statuses = device.statuses(&pool).await.unwrap(); + assert_eq!(statuses.len(), 2); + + let names: Vec = statuses.iter().map(|s| s.name()).collect(); + assert!(names.contains(&"collectiveidea/bar".to_string())); + assert!(names.contains(&"deadmanssnitch/foo".to_string())); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_statuses_by_username(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("inchworm"), Some("foo"), false, false).await; + + let device_id = common::create_device( + &pool, "Test Device", None, &["collectiveidea", "inchworm"], &[], None, None, + ).await; + + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(device_id) + .fetch_one(&pool) + .await + .unwrap(); + + let statuses = device.statuses(&pool).await.unwrap(); + assert_eq!(statuses.len(), 3); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_statuses_by_username_and_project(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("bar"), false, false).await; + common::create_status(&pool, "travis", Some("inchworm"), Some("foo"), false, false).await; + + let device_id = common::create_device( + &pool, "Test Device", None, &["collectiveidea"], &["deadmanssnitch/bar"], None, None, + ).await; + + let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(device_id) + .fetch_one(&pool) + .await + .unwrap(); + + let statuses = device.statuses(&pool).await.unwrap(); + assert_eq!(statuses.len(), 3); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_current_status(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), Some("bar"), false, true).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("foo"), false, false).await; + common::create_status(&pool, "travis", Some("deadmanssnitch"), Some("bar"), true, true).await; + + let device_id = common::create_device( + &pool, "Test Device", None, &["collectiveidea"], &["deadmanssnitch/foo"], None, None, + ).await; + + let mut device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(device_id) + .fetch_one(&pool) + .await + .unwrap(); + + device.update_status(&pool).await.unwrap(); + assert_eq!(device.status.as_deref(), Some("passing-building")); + + // Add a failing status + common::create_status(&pool, "travis", Some("collectiveidea"), Some("baz"), true, false).await; + + let mut device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1") + .bind(device_id) + .fetch_one(&pool) + .await + .unwrap(); + + device.update_status(&pool).await.unwrap(); + assert_eq!(device.status.as_deref(), Some("failing-building")); +} diff --git a/tests/devices_tests.rs b/tests/devices_tests.rs new file mode 100644 index 0000000..721a47e --- /dev/null +++ b/tests/devices_tests.rs @@ -0,0 +1,57 @@ +mod common; + +use axum::body::Body; +use axum::http::Request; +use http_body_util::BodyExt; +use sqlx::PgPool; +use tower::ServiceExt; + +#[sqlx::test(migrations = "./migrations")] +async fn test_show_device_by_id(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, false).await; + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, false).await; + let device_id = common::create_device( + &pool, "Test", Some("test-slug"), &["collectiveidea"], &[], None, None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri(format!("/devices/{device_id}.json")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["red"], false); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_show_device_by_slug(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, false).await; + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, false).await; + let _device_id = common::create_device( + &pool, "Test", Some("test-slug"), &["collectiveidea"], &[], None, None, + ).await; + + let app = common::test_app(pool); + + let response = app + .oneshot( + Request::builder() + .uri("/devices/test-slug.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["red"], false); +} diff --git a/spec/fixtures/circle.json b/tests/fixtures/circle.json similarity index 100% rename from spec/fixtures/circle.json rename to tests/fixtures/circle.json diff --git a/spec/fixtures/circle_pr.json b/tests/fixtures/circle_pr.json similarity index 100% rename from spec/fixtures/circle_pr.json rename to tests/fixtures/circle_pr.json diff --git a/spec/fixtures/github.json b/tests/fixtures/github.json similarity index 100% rename from spec/fixtures/github.json rename to tests/fixtures/github.json diff --git a/spec/fixtures/travis.json b/tests/fixtures/travis.json similarity index 100% rename from spec/fixtures/travis.json rename to tests/fixtures/travis.json diff --git a/tests/parse_tests.rs b/tests/parse_tests.rs new file mode 100644 index 0000000..ff3cedf --- /dev/null +++ b/tests/parse_tests.rs @@ -0,0 +1,111 @@ +mod common; + +use buildlight::models::status::Status; +use buildlight::services::{parse_circle, parse_github, parse_travis}; +use sqlx::PgPool; + +// --- GitHub --- + +#[sqlx::test(migrations = "./migrations")] +async fn test_github_workflow_differentiates(pool: PgPool) { + // Create an existing status with a different workflow + common::create_status_full( + &pool, "github", None, Some("collectiveidea"), Some("buildlight"), + Some("Other Workflow"), true, false, + ).await; + + let fixture: serde_json::Value = + serde_json::from_str(include_str!("fixtures/github.json")).unwrap(); + let payload = parse_github::GitHubPayload { + status: fixture["status"].as_str().unwrap().to_string(), + repository: fixture["repository"].as_str().unwrap().to_string(), + workflow: fixture["workflow"].as_str().map(String::from), + }; + + parse_github::call(&pool, &payload).await.unwrap(); + + // The other_status should still be red + let other = sqlx::query_as::<_, Status>( + "SELECT * FROM statuses WHERE service = 'github' AND workflow = 'Other Workflow'", + ) + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(other.red, Some(true)); + + // There should be two github statuses for this project + let count: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM statuses WHERE service = 'github' AND username = 'collectiveidea' AND project_name = 'buildlight'", + ) + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(count.0, 2); +} + +#[test] +fn test_github_set_colors_success() { + let (red, yellow) = parse_github::set_colors("success"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_github_set_colors_failure() { + let (red, yellow) = parse_github::set_colors("failure"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_github_set_colors_empty() { + let (red, yellow) = parse_github::set_colors(""); + assert_eq!(red, None); + assert_eq!(yellow, Some(true)); +} + +// --- Travis --- + +#[test] +fn test_travis_set_colors_passed() { + let (red, yellow) = parse_travis::set_colors("Passed"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_travis_set_colors_fixed() { + let (red, yellow) = parse_travis::set_colors("Fixed"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_travis_set_colors_still_failing() { + let (red, yellow) = parse_travis::set_colors("Still Failing"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_travis_set_colors_pending() { + let (red, yellow) = parse_travis::set_colors("Pending"); + assert_eq!(red, None); + assert_eq!(yellow, Some(true)); +} + +// --- CircleCI --- + +#[test] +fn test_circle_set_colors_success() { + let (red, yellow) = parse_circle::set_colors("success"); + assert_eq!(red, Some(false)); + assert_eq!(yellow, Some(false)); +} + +#[test] +fn test_circle_set_colors_failed() { + let (red, yellow) = parse_circle::set_colors("failed"); + assert_eq!(red, Some(true)); + assert_eq!(yellow, Some(false)); +} diff --git a/tests/status_tests.rs b/tests/status_tests.rs new file mode 100644 index 0000000..b9ff35e --- /dev/null +++ b/tests/status_tests.rs @@ -0,0 +1,131 @@ +mod common; + +use buildlight::models::status::Status; +use sqlx::PgPool; + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_red_count(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + let colors = Status::colors(&pool, None).await.unwrap(); + assert_eq!(colors.red, serde_json::json!(1)); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_as_booleans_red(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + let colors = Status::colors_as_booleans(&pool, None).await.unwrap(); + assert!(colors.red); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_red_and_yellow(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + common::create_status(&pool, "travis", None, None, false, true).await; + common::create_status(&pool, "travis", None, None, false, true).await; + let colors = Status::colors(&pool, None).await.unwrap(); + assert!(colors.yellow); + assert!(colors.red.is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_another_project_red(pool: PgPool) { + common::create_status(&pool, "travis", None, None, true, false).await; + common::create_status(&pool, "travis", None, None, false, false).await; + let colors = Status::colors(&pool, None).await.unwrap(); + assert!(colors.red.is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_with_username(pool: PgPool) { + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, true).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, true, false).await; + let colors = Status::colors(&pool, Some(&["collectiveidea".into()])).await.unwrap(); + assert!(colors.red.is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_with_username_red_and_yellow(pool: PgPool) { + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, true).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, true, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, true).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, true).await; + let colors = Status::colors(&pool, Some(&["collectiveidea".into()])).await.unwrap(); + assert!(colors.yellow); + assert!(colors.red.is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_with_username_another_project_red(pool: PgPool) { + common::create_status(&pool, "travis", Some("danielmorrison"), None, true, true).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, true, false).await; + common::create_status(&pool, "travis", Some("collectiveidea"), None, false, false).await; + let colors = Status::colors(&pool, Some(&["collectiveidea".into()])).await.unwrap(); + assert!(colors.red.is_number()); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_colors_with_multiple_usernames(pool: PgPool) { + common::create_status(&pool, "travis", Some("collectiveidea"), None, true, false).await; + common::create_status(&pool, "travis", Some("danielmorrison"), None, false, true).await; + let colors = Status::colors( + &pool, + Some(&["collectiveidea".into(), "danielmorrison".into()]), + ) + .await + .unwrap(); + assert!(colors.red.is_number()); + assert!(colors.yellow); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_name(pool: PgPool) { + let status = Status { + id: 1, + project_id: None, + project_name: Some("foo".into()), + created_at: None, + updated_at: None, + payload: None, + red: Some(false), + yellow: Some(false), + username: Some("collectiveidea".into()), + service: "github".into(), + workflow: None, + }; + assert_eq!(status.name(), "collectiveidea/foo"); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_devices(pool: PgPool) { + let _d1 = common::create_device( + &pool, "D1", None, &["collectiveidea"], &["deadmanssnitch/foo"], None, None, + ).await; + let _d2 = common::create_device( + &pool, "D2", None, &["collectiveidea", "deadmanssnitch"], &[], None, None, + ).await; + let _d3 = common::create_device( + &pool, "D3", None, &["deadmanssnitch"], &["collectiveidea/foo"], None, None, + ).await; + let _d4 = common::create_device( + &pool, "D4", None, &[], &["collectiveidea/foo"], None, None, + ).await; + let _d5 = common::create_device( + &pool, "D5", None, &["deadmanssnitch"], &[], None, None, + ).await; + + common::create_status(&pool, "travis", Some("collectiveidea"), Some("foo"), false, false).await; + + let status = sqlx::query_as::<_, Status>("SELECT * FROM statuses ORDER BY id DESC LIMIT 1") + .fetch_one(&pool) + .await + .unwrap(); + + let devices = status.devices(&pool).await.unwrap(); + let device_names: Vec<&str> = devices.iter().map(|d| d.name.as_str()).collect(); + + assert!(device_names.contains(&"D1")); + assert!(device_names.contains(&"D2")); + assert!(device_names.contains(&"D3")); + assert!(device_names.contains(&"D4")); + assert!(!device_names.contains(&"D5")); +} diff --git a/tests/webhook_tests.rs b/tests/webhook_tests.rs new file mode 100644 index 0000000..fbf86bb --- /dev/null +++ b/tests/webhook_tests.rs @@ -0,0 +1,169 @@ +mod common; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use sqlx::PgPool; +use tower::ServiceExt; + +fn travis_fixture() -> String { + include_str!("fixtures/travis.json").to_string() +} + +fn github_fixture() -> &'static str { + include_str!("fixtures/github.json") +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_unknown_data_returns_bad_request(pool: PgPool) { + let app = common::test_app(pool.clone()); + + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM statuses") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(count.0, 0); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(r#"{"foo": "bar"}"#)) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM statuses") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(count.0, 0); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_travis_webhook(pool: PgPool) { + let app = common::test_app(pool.clone()); + + let body = serde_json::json!({"payload": travis_fixture()}); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(body.to_string())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_travis_saves_data(pool: PgPool) { + let app = common::test_app(pool.clone()); + + let body = serde_json::json!({"payload": travis_fixture()}); + app.oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(body.to_string())) + .unwrap(), + ) + .await + .unwrap(); + + let status = sqlx::query_as::<_, buildlight::models::status::Status>( + "SELECT * FROM statuses ORDER BY created_at DESC LIMIT 1", + ) + .fetch_one(&pool) + .await + .unwrap(); + + assert_eq!(status.red, Some(false)); + assert_eq!(status.service, "travis"); + assert_eq!(status.project_id.as_deref(), Some("347744")); + assert_eq!(status.project_name.as_deref(), Some("buildlight")); + assert_eq!(status.username.as_deref(), Some("collectiveidea")); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_travis_ignores_pull_requests(pool: PgPool) { + let app = common::test_app(pool.clone()); + + let fixture = travis_fixture().replace(r#""type":"push""#, r#""type":"pull_request""#); + let body = serde_json::json!({"payload": fixture}); + app.oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(body.to_string())) + .unwrap(), + ) + .await + .unwrap(); + + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM statuses") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(count.0, 0); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_github_webhook(pool: PgPool) { + let app = common::test_app(pool.clone()); + let payload: serde_json::Value = serde_json::from_str(github_fixture()).unwrap(); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(payload.to_string())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[sqlx::test(migrations = "./migrations")] +async fn test_github_saves_data(pool: PgPool) { + let app = common::test_app(pool.clone()); + let payload: serde_json::Value = serde_json::from_str(github_fixture()).unwrap(); + + app.oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("content-type", "application/json") + .body(Body::from(payload.to_string())) + .unwrap(), + ) + .await + .unwrap(); + + let status = sqlx::query_as::<_, buildlight::models::status::Status>( + "SELECT * FROM statuses ORDER BY created_at DESC LIMIT 1", + ) + .fetch_one(&pool) + .await + .unwrap(); + + assert_eq!(status.red, Some(false)); + assert_eq!(status.service, "github"); + assert_eq!(status.project_id, None); + assert_eq!(status.project_name.as_deref(), Some("buildlight")); + assert_eq!(status.username.as_deref(), Some("collectiveidea")); +} diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep deleted file mode 100644 index e69de29..0000000