From b68d4d249813481f9a5ac6909dc046d2b621bac3 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 29 Jul 2026 14:11:59 -0700 Subject: [PATCH 1/5] rust first pass --- .github/workflows/ci.yml | 97 +++++ .gitignore | 5 + CHANGELOG.md | 10 +- Cargo.lock | 885 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + DEVELOPMENT.md | 77 +++- Gemfile | 2 +- Gemfile.lock | 126 +++++- README.md | 89 +++- Rakefile | 66 +-- dotenvx-rails.gemspec | 15 +- dotenvx.gemspec | 23 +- ext/dotenvx/Cargo.toml | 15 + ext/dotenvx/extconf.rb | 6 + ext/dotenvx/src/lib.rs | 68 +++ lib/dotenvx-rails.rb | 1 + lib/dotenvx.rb | 105 ++++- lib/dotenvx/load.rb | 5 + lib/dotenvx/rails.rb | 26 +- lib/dotenvx/version.rb | 2 +- spec/dotenvx_spec.rb | 62 +++ spec/spec_helper.rb | 4 +- 22 files changed, 1619 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 ext/dotenvx/Cargo.toml create mode 100644 ext/dotenvx/extconf.rb create mode 100644 ext/dotenvx/src/lib.rs create mode 100644 lib/dotenvx/load.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d113918 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: ci + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + ruby-version: ["3.1", "3.4"] + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - uses: dtolnay/rust-toolchain@stable + - run: bundle exec rake + - run: cargo fmt --check + - run: cargo clippy --workspace --all-features --locked -- -D warnings + + native-gems: + if: startsWith(github.ref, 'refs/tags/v') + needs: test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact: x86_64-linux + - os: ubuntu-24.04-arm + artifact: aarch64-linux + - os: macos-15-intel + artifact: x86_64-darwin + - os: macos-14 + artifact: arm64-darwin + - os: windows-latest + artifact: x64-mingw-ucrt + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.1" + bundler-cache: true + - uses: dtolnay/rust-toolchain@stable + - name: Check tag version + shell: bash + run: | + expected="${GITHUB_REF_NAME#v}" + actual="$(ruby -Ilib -r dotenvx/version -e 'print Dotenvx::VERSION')" + test "$actual" = "$expected" + - run: bundle exec rake package:native + - name: Test packaged gem + shell: bash + run: | + gem install --local --ignore-dependencies pkg/*.gem + ruby -e 'require "dotenvx"; abort unless Dotenvx::VERSION == ENV.fetch("GITHUB_REF_NAME").delete_prefix("v")' + - uses: actions/upload-artifact@v7 + with: + name: gem-${{ matrix.artifact }} + path: pkg/*.gem + + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: native-gems + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + steps: + - uses: actions/download-artifact@v6 + with: + pattern: gem-* + path: pkg + merge-multiple: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + - name: Configure RubyGems credentials + uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0 + - name: Publish platform gems + run: | + for gem in pkg/*.gem; do + gem push "$gem" + done diff --git a/.gitignore b/.gitignore index 92faf71..7be5d48 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ /pkg/ /spec/reports/ /tmp/ +/target/ +/lib/dotenvx/dotenvx_native.bundle +/lib/dotenvx/dotenvx_native.dll +/lib/dotenvx/dotenvx_native.so +/ext/dotenvx/Makefile # rspec failure tracking .rspec_status diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a74dfd..65c2011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/dotenvx/dotenvx-ruby/compare/v0.1.0...main) +## [Unreleased](https://github.com/dotenvx/dotenvx-ruby/compare/v2.0.0...main) + +## [2.0.0](https://github.com/dotenvx/dotenvx-ruby/compare/v0.0.2...v2.0.0) + +- Load plaintext and encrypted dotenv files through `dotenvx-primitives`. +- Bundle the Rust implementation as a Magnus native extension. +- Match the conventional Ruby dotenv loading, parsing, overwrite, and strict + missing-file APIs. +- Publish native `dotenvx` gems for Linux, macOS, and Windows from one tag. ## 0.1.0 and prior diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..07fff0a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,885 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[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 = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[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 = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvx-primitives" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d968bcad44f3ac36256fe6dcf4f21a9a0a19a38769ab73e0081575f4e0cc910" +dependencies = [ + "base64", + "ecies", + "hex", + "rand", + "regex", + "zeroize", +] + +[[package]] +name = "dotenvx_native" +version = "2.0.0" +dependencies = [ + "dotenvx-primitives", + "magnus", + "rb-sys", +] + +[[package]] +name = "ecies" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2df85e2b13a5f7fa61e7f0ee776acfe74da80bf2fb1bcde2725af19c197b33" +dependencies = [ + "aes-gcm", + "getrandom", + "hkdf", + "k256", + "lock_api", + "once_cell", + "parking_lot", + "rand_core", + "sha2", + "typenum", + "wasm-bindgen", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "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", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[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 = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", + "once_cell", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "magnus" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012" +dependencies = [ + "magnus-macros", + "rb-sys", + "rb-sys-env", + "seq-macro", +] + +[[package]] +name = "magnus-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rb-sys" +version = "0.9.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +dependencies = [ + "rb-sys-build", +] + +[[package]] +name = "rb-sys-build" +version = "0.9.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +dependencies = [ + "bindgen", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "shell-words", + "syn", +] + +[[package]] +name = "rb-sys-env" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[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 = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..65e9aab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["ext/dotenvx"] +resolver = "2" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bce2f2a..b194d2c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,4 +1,75 @@ -## Development +# Development -1. Bump `dotenvx/version.rb` and tag version -2. rake release +`dotenvx` is a mixed Ruby/Rust package. Magnus exposes the published +`dotenvx-primitives` crate to Ruby, and `rb-sys` compiles the extension into +`lib/dotenvx`. + +## Setup + +```sh +bundle install +bundle exec rake compile +bundle exec rspec +``` + +Validate Rust: + +```sh +cargo fmt --check +cargo clippy --workspace --all-features --locked -- -D warnings +cargo test --workspace --all-features --locked +cargo audit +``` + +Build and inspect the native gem for the current platform: + +```sh +bundle exec rake package:native +gem specification pkg/dotenvx-*.gem +``` + +## Versions + +The Ruby gem and native crate versions must match. Change both: + +```text +lib/dotenvx/version.rb +ext/dotenvx/Cargo.toml +``` + +Then refresh dependency locks: + +```sh +cargo check +bundle install +``` + +The `dotenvx-primitives` dependency is versioned independently. Update it in +`ext/dotenvx/Cargo.toml` only when the gem should embed a newer primitives +release. + +## Publishing + +Tags matching `v*` run the full test matrix, build five platform variants of +the existing `dotenvx` gem, and publish all of them to RubyGems. No additional +gem is published. + +Before the first automated release, add a trusted publisher for the `dotenvx` +gem on RubyGems.org with: + +- repository owner: `dotenvx` +- repository name: `dotenvx-ruby` +- workflow filename: `ci.yml` +- environment: `release` + +The workflow uses a short-lived RubyGems credential through GitHub OIDC, so no +long-lived API-key secret is needed. + +After updating both version sources and the changelog: + +```sh +git tag v2.0.0 +git push origin v2.0.0 +``` + +The tag must match `Dotenvx::VERSION`. CI verifies this before packaging. diff --git a/Gemfile b/Gemfile index 156a62c..52d1403 100644 --- a/Gemfile +++ b/Gemfile @@ -4,5 +4,5 @@ source "https://rubygems.org" gemspec name: "dotenvx" gemspec name: "dotenvx-rails" -gem "rake", "~> 12.0" +gem "rake", "~> 13.0" gem "rspec", "~> 3.0" diff --git a/Gemfile.lock b/Gemfile.lock index 4614b7f..e3da520 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,16 +1,123 @@ PATH remote: . specs: - dotenvx (0.0.1) - dotenvx-rails (0.0.1) - dotenvx (= 0.0.1) + dotenvx (2.0.0) + rb_sys (~> 0.9) + dotenvx-rails (2.0.0) + dotenvx (= 2.0.0) + railties (>= 5.0) GEM remote: https://rubygems.org/ specs: + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) + 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) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (8.1.3.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.3.0) + bigdecimal (4.1.2) + builder (3.3.0) byebug (11.1.3) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) diff-lcs (1.5.1) - rake (12.3.3) + drb (2.2.3) + erb (6.0.6) + erubi (1.13.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.21.1) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mini_portile2 (2.8.9) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + 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.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rake (13.4.2) + rake-compiler (1.3.1) + rake + rake-compiler-dock (1.12.0) + rb_sys (0.9.128) + rake-compiler-dock (= 1.12.0) + rbs (4.1.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.6.3) + io-console (~> 0.5) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -24,7 +131,15 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.1) + securerandom (0.4.1) spring (4.2.1) + thor (1.5.0) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.1.1) + useragent (0.16.11) + zeitwerk (2.8.2) PLATFORMS arm64-darwin-23 @@ -34,7 +149,8 @@ DEPENDENCIES byebug dotenvx! dotenvx-rails! - rake (~> 12.0) + rake (~> 13.0) + rake-compiler (~> 1.3) rspec (~> 3.0) spring diff --git a/README.md b/README.md index 2782489..be4d279 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,83 @@ -> [!IMPORTANT] -> -> Warning: work in progress. until complete, please use [github.com/dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) directly. -> -> see [ruby examples](https://dotenvx.com/docs/languages/ruby) -> +[![dotenvx](https://dotenvx.com/better-banner.png)](https://dotenvx.com) ---- +*a secure dotenv–from the creator of [`dotenv`](https://github.com/motdotla/dotenv).* -[![dotenvx](https://dotenvx.com/better-banner.png)](https://dotenvx.com) +`dotenvx` loads plaintext and encrypted dotenv files through the shared +[`dotenvx-primitives`](https://crates.io/crates/dotenvx-primitives) Rust +implementation. The native extension is bundled in the gem; users do not need +Node.js, a dotenvx CLI, or a Rust toolchain. -*a better dotenv*–from the creator of [`dotenv`](https://github.com/motdotla/dotenv). +## Install -* run anywhere (cross-platform) -* multi-environment -* encrypted envs +```sh +gem install dotenvx +``` -  +Or add it to a Gemfile: +```ruby +gem "dotenvx" +``` -### Quickstart [![Gem Version](https://badge.fury.io/rb/dotenvx.svg)](https://badge.fury.io/rb/dotenvx) +## Use -Install and use it in code just like `dotenv`. +Load `.env` from the current directory: -```sh -gem install dotenvx -``` ```ruby -# index.rb require "dotenvx/load" +``` + +Or load explicitly: -puts "Hello #{ENV["HELLO"]}" +```ruby +require "dotenvx" + +Dotenvx.load +Dotenvx.load(".env.local", ".env") ``` -  +Existing environment variables win by default: + +```ruby +Dotenvx.load(".env") +``` + +To replace existing values: + +```ruby +Dotenvx.load(".env", overwrite: true) +Dotenvx.overwrite(".env") +``` + +Parse without changing `ENV`: + +```ruby +values = Dotenvx.parse(".env") +``` + +Raise when a file is missing: + +```ruby +Dotenvx.load!(".env") +``` + +Require configuration keys: + +```ruby +Dotenvx.require_keys("DATABASE_URL", "SECRET_KEY") +``` + +Encrypted values are decrypted automatically when the matching private key is +available through the process environment, `.keys`, or `.env.keys`. + +## Native platforms + +Tagged releases publish variants of the same `dotenvx` gem for: + +- Linux x86-64 +- Linux ARM64 +- macOS Intel +- macOS Apple Silicon +- Windows x64 +RubyGems selects the correct variant during `gem install dotenvx`. diff --git a/Rakefile b/Rakefile index 9235805..8cfaa1e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,28 +1,9 @@ #!/usr/bin/env rake -require "bundler/gem_helper" - -namespace "dotenvx" do - Bundler::GemHelper.install_tasks name: "dotenvx" -end - -class DotenvxRailsGemHelper < Bundler::GemHelper - def guard_already_tagged - # noop - end - - def tag_version - # noop - end -end - -namespace "dotenvx-rails" do - DotenvxRailsGemHelper.install_tasks name: "dotenvx-rails" -end - -task build: ["dotenvx:build", "dotenvx-rails:build"] -task install: ["dotenvx:install", "dotenvx-rails:install"] -task release: ["dotenvx:release", "dotenvx-rails:release"] +require "rb_sys/extensiontask" +require "fileutils" +require "rbconfig" +require "rubygems/package" require "rspec/core/rake_task" @@ -32,4 +13,41 @@ RSpec::Core::RakeTask.new(:spec) do |t| t.verbose = false end -task :default => :spec +gemspec = Gem::Specification.load("dotenvx.gemspec") + +RbSys::ExtensionTask.new("dotenvx_native", gemspec) do |ext| + ext.ext_dir = "ext/dotenvx" + ext.lib_dir = "lib/dotenvx" +end + +task build: :compile +task default: %i[compile spec] + +namespace :package do + desc "Build the platform-specific dotenvx gem with its Rust extension" + task native: :compile do + local = Gem::Platform.local + platform = if local.os == "darwin" + Gem::Platform.new([local.cpu, local.os, nil]) + else + local + end + extension = "lib/dotenvx/dotenvx_native.#{RbConfig::CONFIG.fetch("DLEXT")}" + abort "missing compiled extension: #{extension}" unless File.file?(extension) + + native_spec = gemspec.dup + native_spec.platform = platform + native_spec.extensions = [] + native_spec.files = native_spec.files.reject do |file| + file == "Cargo.toml" || + file == "Cargo.lock" || + file.start_with?("ext/dotenvx/") + end + native_spec.files |= [extension] + native_spec.dependencies.delete_if { |dependency| dependency.name == "rb_sys" } + + FileUtils.mkdir_p("pkg") + gem_file = Gem::Package.build(native_spec) + FileUtils.mv(gem_file, File.join("pkg", File.basename(gem_file))) + end +end diff --git a/dotenvx-rails.gemspec b/dotenvx-rails.gemspec index 60f6bad..1a202be 100644 --- a/dotenvx-rails.gemspec +++ b/dotenvx-rails.gemspec @@ -10,22 +10,25 @@ Gem::Specification.new "dotenvx-rails" do |spec| spec.description = %q{[dotenvx.com] a better dotenv–from the creator of `dotenv`} spec.homepage = "https://github.com/dotenvx/dotenvx-ruby" spec.license = "BSD-3-Clause" - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0") spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/dotenvx/dotenvx-ruby" spec.metadata["changelog_uri"] = "https://github.com/dotenvx/dotenvx-ruby" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - end + spec.files = [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "lib/dotenvx-rails.rb", + "lib/dotenvx/rails.rb" + ] spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "dotenvx", Dotenvx::VERSION + spec.add_dependency "railties", ">= 5.0" spec.add_development_dependency "spring" spec.add_development_dependency "byebug" diff --git a/dotenvx.gemspec b/dotenvx.gemspec index 278883c..29e0553 100644 --- a/dotenvx.gemspec +++ b/dotenvx.gemspec @@ -10,22 +10,33 @@ Gem::Specification.new "dotenvx" do |spec| spec.description = %q{[dotenvx.com] a better dotenv–from the creator of `dotenv`} spec.homepage = "https://github.com/dotenvx/dotenvx-ruby" spec.license = "BSD-3-Clause" - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0") + spec.required_rubygems_version = Gem::Requirement.new(">= 3.3.11") spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/dotenvx/dotenvx-ruby" spec.metadata["changelog_uri"] = "https://github.com/dotenvx/dotenvx-ruby" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = Dir[ + "Cargo.lock", + "Cargo.toml", + "CHANGELOG.md", + "LICENSE", + "README.md", + "ext/dotenvx/**/*", + "lib/dotenvx.rb", + "lib/dotenvx/**/*" + ].reject do |file| + File.directory?(file) || file.match?(/dotenvx_native\.(bundle|dll|so)\z/) end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.extensions = ["ext/dotenvx/extconf.rb"] - spec.add_development_dependency "rake" + spec.add_dependency "rb_sys", "~> 0.9" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rake-compiler", "~> 1.3" spec.add_development_dependency "rspec" spec.add_development_dependency "byebug" end diff --git a/ext/dotenvx/Cargo.toml b/ext/dotenvx/Cargo.toml new file mode 100644 index 0000000..d786196 --- /dev/null +++ b/ext/dotenvx/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dotenvx_native" +version = "2.0.0" +edition = "2021" +rust-version = "1.83" +publish = false + +[lib] +name = "dotenvx_native" +crate-type = ["cdylib"] + +[dependencies] +dotenvx-primitives = "=2.1.2" +magnus = "0.8" +rb-sys = { version = "0.9.128", default-features = false, features = ["stable-api-compiled-fallback"] } diff --git a/ext/dotenvx/extconf.rb b/ext/dotenvx/extconf.rb new file mode 100644 index 0000000..8c99659 --- /dev/null +++ b/ext/dotenvx/extconf.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile("dotenvx/dotenvx_native") diff --git a/ext/dotenvx/src/lib.rs b/ext/dotenvx/src/lib.rs new file mode 100644 index 0000000..88c2eab --- /dev/null +++ b/ext/dotenvx/src/lib.rs @@ -0,0 +1,68 @@ +use dotenvx_primitives::{keyring, parse, KeyringOptions, ParseOptions, ParseResult, Value}; +use magnus::{function, prelude::*, Error, Ruby}; +use std::collections::HashMap; +use std::path::PathBuf; + +type StringPairs = Vec<(String, String)>; + +fn runtime_error(message: impl Into) -> Error { + let ruby = Ruby::get().expect("Ruby VM is not available"); + Error::new(ruby.exception_runtime_error(), message.into()) +} + +fn scalar_values(values: HashMap) -> StringPairs { + values + .into_iter() + .filter_map(|(key, value)| match value { + Value::Scalar(value) => Some((key, value)), + Value::Array(_) => None, + }) + .collect() +} + +fn parse_result(result: ParseResult) -> Result<(StringPairs, StringPairs), Error> { + if !result.errors.is_empty() { + let message = result + .errors + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + return Err(runtime_error(message)); + } + + Ok((scalar_values(result.parsed), scalar_values(result.injected))) +} + +fn parse_dotenv( + source: String, + process_env: StringPairs, + overwrite: bool, + key_files: Vec, +) -> Result<(StringPairs, StringPairs), Error> { + let process_env = process_env.into_iter().collect::>(); + let ring = keyring(&KeyringOptions { + process_env: process_env.clone(), + key_files: key_files.into_iter().map(PathBuf::from).collect(), + ..Default::default() + }) + .map_err(|error| runtime_error(error.to_string()))?; + + parse_result(parse( + &source, + &ParseOptions { + process_env, + overload: overwrite, + ring, + ..Default::default() + }, + )) +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let dotenvx = ruby.define_module("Dotenvx")?; + let native = dotenvx.define_module("Native")?; + native.define_singleton_method("parse_dotenv", function!(parse_dotenv, 4))?; + Ok(()) +} diff --git a/lib/dotenvx-rails.rb b/lib/dotenvx-rails.rb index 932e631..25bd4a4 100644 --- a/lib/dotenvx-rails.rb +++ b/lib/dotenvx-rails.rb @@ -1 +1,2 @@ +require "dotenvx" require "dotenvx/rails" diff --git a/lib/dotenvx.rb b/lib/dotenvx.rb index 4e4a18d..601be4c 100644 --- a/lib/dotenvx.rb +++ b/lib/dotenvx.rb @@ -1,7 +1,108 @@ require "dotenvx/version" +require "dotenvx/dotenvx_native" module Dotenvx - def load(*filenames) - raise "go to [github.com/dotenvx/dotenvx] and follow ruby directions there" + class MissingKeys < RuntimeError + attr_reader :keys + + def initialize(keys) + @keys = keys + super("Missing required configuration keys: #{keys.join(", ")}") + end + end + + class << self + attr_accessor :instrumenter + + def load(*filenames, overwrite: false, ignore: true) + env = parse(*filenames, overwrite: overwrite, ignore: ignore) + update(env, overwrite: overwrite) + end + + def load!(*filenames) + load(*filenames, ignore: false) + end + + def overwrite(*filenames) + load(*filenames, overwrite: true) + end + alias overload overwrite + + def overwrite!(*filenames) + load(*filenames, overwrite: true, ignore: false) + end + alias overload! overwrite! + + def parse(*filenames, overwrite: false, ignore: true) + filenames = [".env"] if filenames.empty? + filenames = filenames.flatten.reverse if overwrite + + process_env = ENV.to_h + filenames.reduce({}) do |values, filename| + path = File.expand_path(filename) + begin + source = File.binread(path).sub(/\A\xEF\xBB\xBF/, "").force_encoding(Encoding::UTF_8) + rescue Errno::ENOENT, Errno::EISDIR + raise unless ignore + next values + end + + parsed, = Native.parse_dotenv( + source, + process_env.to_a, + overwrite == true, + key_files(path) + ) + parsed = parsed.to_h + process_env.merge!(parsed) + values.merge!(parsed) + yield Environment.new(path, parsed) if block_given? + values + end + end + + def update(env = {}, overwrite: false) + changed = {} + env.each do |key, value| + key = key.to_s + next if ENV.key?(key) && overwrite == false + + if ENV.key?(key) && overwrite == :warn + warn "Warning: dotenvx not overwriting ENV[#{key.inspect}]" + next + end + unless [true, false, :warn].include?(overwrite) + raise ArgumentError, "Invalid value for overwrite: #{overwrite.inspect}" + end + + ENV[key] = value.to_s + changed[key] = value.to_s + end + changed + end + + def require_keys(*keys) + missing = keys.flatten.map(&:to_s) - ENV.keys + raise MissingKeys, missing unless missing.empty? + end + + private + + def key_files(path) + basename = File.basename(path) + candidates = ["#{path}.keys"] + candidates << File.join(File.dirname(path), ".env.keys") unless basename == ".env" + candidates.select { |candidate| File.file?(candidate) } + end + end + + class Environment < Hash + attr_reader :filename + + def initialize(filename, values) + @filename = filename + super() + update(values) + end end end diff --git a/lib/dotenvx/load.rb b/lib/dotenvx/load.rb new file mode 100644 index 0000000..3638d59 --- /dev/null +++ b/lib/dotenvx/load.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "dotenvx" + +Dotenvx.load diff --git a/lib/dotenvx/rails.rb b/lib/dotenvx/rails.rb index 2fdd6c4..71515f4 100644 --- a/lib/dotenvx/rails.rb +++ b/lib/dotenvx/rails.rb @@ -1,4 +1,7 @@ require "dotenvx" +require "active_support/notifications" +require "rails/railtie" +require "pathname" # Fix for rake tasks loading in development # @@ -30,9 +33,30 @@ end module Dotenvx - class Railtie + class Railtie < Rails::Railtie + config.before_configuration { Dotenvx::Railtie.instance.load } + def load Dotenvx.load(*dotenvx_files) end + + private + + def dotenvx_files + environment = Rails.env + files = [ + root.join(".env.#{environment}.local") + ] + files << root.join(".env.local") unless environment == "test" + files.concat([ + root.join(".env.#{environment}"), + root.join(".env") + ]) + files + end + + def root + Pathname.new(Rails.root || ENV["RAILS_ROOT"] || Dir.pwd) + end end end diff --git a/lib/dotenvx/version.rb b/lib/dotenvx/version.rb index cb02507..d20dab2 100644 --- a/lib/dotenvx/version.rb +++ b/lib/dotenvx/version.rb @@ -1,3 +1,3 @@ module Dotenvx - VERSION = "0.0.2" + VERSION = "2.0.0" end diff --git a/spec/dotenvx_spec.rb b/spec/dotenvx_spec.rb index f84b86a..83631ea 100644 --- a/spec/dotenvx_spec.rb +++ b/spec/dotenvx_spec.rb @@ -1,4 +1,66 @@ require "spec_helper" +require "tmpdir" RSpec.describe Dotenvx do + let(:private_key) { "a4547dcd9d3429615a3649bb79e87edb62ee6a74b007075e9141ae44f5fb412c" } + let(:encrypted_world) do + "encrypted:BE9Y7LKANx77X1pv1HnEoil93fPa5c9rpL/1ps48uaRT9zM8VR6mHx9yM+HktKdsPGIZELuZ7rr2mn1gScsmWitppAgE/1lVprNYBCqiYeaTcKXjDUXU5LfsEsflnAsDhT/kWG1l" + end + + it "loads an explicit dotenv file without replacing existing values" do + with_env_file("HELLO=World\nEXISTING=file\n") do |path| + ENV["EXISTING"] = "environment" + + expect(described_class.load(path)).to eq("HELLO" => "World") + expect(ENV["HELLO"]).to eq("World") + expect(ENV["EXISTING"]).to eq("environment") + end + end + + it "supports dotenv-compatible overwrite aliases" do + with_env_file("EXISTING=file\n") do |path| + ENV["EXISTING"] = "environment" + + expect(described_class.overwrite(path)).to eq("EXISTING" => "file") + expect(ENV["EXISTING"]).to eq("file") + end + end + + it "parses without changing ENV" do + with_env_file("PARSED=yes\n") do |path| + expect(described_class.parse(path)).to eq("PARSED" => "yes") + expect(ENV).not_to have_key("PARSED") + end + end + + it "decrypts with the adjacent dotenvx key file" do + Dir.mktmpdir do |directory| + path = File.join(directory, ".env") + File.write(path, "HELLO=#{encrypted_world}\n") + File.write("#{path}.keys", "DOTENV_PRIVATE_KEY=#{private_key}\n") + + expect(described_class.load(path)).to eq("HELLO" => "World") + expect(ENV["HELLO"]).to eq("World") + end + end + + it "ignores missing files by default and raises through load!" do + missing = File.join(Dir.tmpdir, "dotenvx-does-not-exist") + + expect(described_class.load(missing)).to eq({}) + expect { described_class.load!(missing) }.to raise_error(Errno::ENOENT) + end + + it "requires configuration keys" do + expect { described_class.require_keys("MISSING") } + .to raise_error(Dotenvx::MissingKeys, /MISSING/) + end + + def with_env_file(contents) + Dir.mktmpdir do |directory| + path = File.join(directory, ".env") + File.write(path, contents) + yield path + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7ad87a3..549bfee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,8 +14,8 @@ end # Restore the state of ENV after each spec - config.before { @env_keys = ENV.keys } - config.after { ENV.delete_if { |k, _v| !@env_keys.include?(k) } } + config.before { @env_before_example = ENV.to_h } + config.after { ENV.replace(@env_before_example) } end def fixture_path(name) From 72eea685a96ba218a4461f7176bec0244e9ff58c Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 29 Jul 2026 16:32:24 -0700 Subject: [PATCH 2/5] fix up --- .github/workflows/ci.yml | 6 ++---- Gemfile | 5 +++++ Gemfile.lock | 41 +++++++++++++++++++++------------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d113918..c80ac04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,11 +20,10 @@ jobs: ruby-version: ["3.1", "3.4"] steps: - uses: actions/checkout@v6 - - uses: ruby/setup-ruby@v1 + - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - - uses: dtolnay/rust-toolchain@stable - run: bundle exec rake - run: cargo fmt --check - run: cargo clippy --workspace --all-features --locked -- -D warnings @@ -49,11 +48,10 @@ jobs: artifact: x64-mingw-ucrt steps: - uses: actions/checkout@v6 - - uses: ruby/setup-ruby@v1 + - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 with: ruby-version: "3.1" bundler-cache: true - - uses: dtolnay/rust-toolchain@stable - name: Check tag version shell: bash run: | diff --git a/Gemfile b/Gemfile index 52d1403..d341756 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,8 @@ gemspec name: "dotenvx-rails" gem "rake", "~> 13.0" gem "rspec", "~> 3.0" + +# Rails 8 requires Ruby 3.2+. Keep development dependencies compatible with +# the oldest Ruby version supported by this gem and exercised in CI. +gem "railties", ">= 5", "< 8" +gem "connection_pool", "< 3" diff --git a/Gemfile.lock b/Gemfile.lock index e3da520..ba4ca6a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,41 +10,45 @@ PATH GEM remote: https://rubygems.org/ specs: - actionpack (8.1.3.1) - actionview (= 8.1.3.1) - activesupport (= 8.1.3.1) + actionpack (7.2.3.2) + actionview (= 7.2.3.2) + activesupport (= 7.2.3.2) + cgi nokogiri (>= 1.8.5) - rack (>= 2.2.4) + racc + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actionview (8.1.3.1) - activesupport (= 8.1.3.1) + actionview (7.2.3.2) + activesupport (= 7.2.3.2) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activesupport (8.1.3.1) + activesupport (7.2.3.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) - json logger (>= 1.4.2) - minitest (>= 5.1) + minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) base64 (0.3.0) + benchmark (0.5.0) bigdecimal (4.1.2) builder (3.3.0) byebug (11.1.3) + cgi (0.5.2) concurrent-ruby (1.3.8) - connection_pool (3.0.2) + connection_pool (2.5.5) crass (1.0.7) diff-lcs (1.5.1) drb (2.2.3) @@ -58,15 +62,12 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.21.1) logger (1.7.0) loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) mini_portile2 (2.8.9) - minitest (6.0.6) - drb (~> 2.0) - prism (~> 1.5) + minitest (5.27.0) nokogiri (1.19.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -92,9 +93,10 @@ GEM rails-html-sanitizer (1.7.1) loofah (~> 2.25, >= 2.25.2) 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.1.3.1) - actionpack (= 8.1.3.1) - activesupport (= 8.1.3.1) + railties (7.2.3.2) + actionpack (= 7.2.3.2) + activesupport (= 7.2.3.2) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -137,7 +139,6 @@ GEM tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uri (1.1.1) useragent (0.16.11) zeitwerk (2.8.2) @@ -147,8 +148,10 @@ PLATFORMS DEPENDENCIES byebug + connection_pool (< 3) dotenvx! dotenvx-rails! + railties (>= 5, < 8) rake (~> 13.0) rake-compiler (~> 1.3) rspec (~> 3.0) From 6082cb7785f132419477105ae1d64eb37ef4e18b Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 29 Jul 2026 16:35:30 -0700 Subject: [PATCH 3/5] fix ruby 3.1 dependency lock --- Gemfile.lock | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba4ca6a..ca2f4de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,9 +50,9 @@ GEM concurrent-ruby (1.3.8) connection_pool (2.5.5) crass (1.0.7) + date (3.5.1) diff-lcs (1.5.1) drb (2.2.3) - erb (6.0.6) erubi (1.13.1) i18n (1.15.2) concurrent-ruby (~> 1.0) @@ -68,15 +68,18 @@ GEM nokogiri (>= 1.12.0) mini_portile2 (2.8.9) minitest (5.27.0) - nokogiri (1.19.4) + nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.19.4-arm64-darwin) + nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) pp (0.6.4) prettyprint prettyprint (0.2.0) prism (1.9.0) + psych (5.4.0) + date + stringio racc (1.8.1) rack (3.2.6) rack-session (2.1.2) @@ -109,15 +112,8 @@ GEM rake-compiler-dock (1.12.0) rb_sys (0.9.128) rake-compiler-dock (= 1.12.0) - rbs (4.1.0) - logger - prism (>= 1.6.0) - tsort - rdoc (8.0.0) - erb - prism (>= 1.6.0) - rbs (>= 4.0.0) - tsort + rdoc (6.13.1) + psych (>= 4.0.0) reline (0.6.3) io-console (~> 0.5) rspec (3.13.0) @@ -135,12 +131,13 @@ GEM rspec-support (3.13.1) securerandom (0.4.1) spring (4.2.1) + stringio (3.2.0) thor (1.5.0) tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) useragent (0.16.11) - zeitwerk (2.8.2) + zeitwerk (2.6.18) PLATFORMS arm64-darwin-23 From 0053a39866b595bc4218dcb818e842e7356159cb Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 29 Jul 2026 16:41:03 -0700 Subject: [PATCH 4/5] lock native dependency platforms --- Gemfile.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index ca2f4de..5b4adb0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,8 +71,16 @@ GEM nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + nokogiri (1.18.10-x64-mingw-ucrt) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) pp (0.6.4) prettyprint prettyprint (0.2.0) @@ -140,8 +148,12 @@ GEM zeitwerk (2.6.18) PLATFORMS + aarch64-linux arm64-darwin-23 ruby + x64-mingw-ucrt + x86_64-darwin + x86_64-linux DEPENDENCIES byebug From 27dd7d778b5abcf55122c1a24270add5e171d928 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 29 Jul 2026 16:52:45 -0700 Subject: [PATCH 5/5] version change --- CHANGELOG.md | 4 ++-- Cargo.lock | 2 +- DEVELOPMENT.md | 4 ++-- Gemfile.lock | 6 +++--- ext/dotenvx/Cargo.toml | 2 +- lib/dotenvx/version.rb | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c2011..fa11a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/dotenvx/dotenvx-ruby/compare/v2.0.0...main) +## [Unreleased](https://github.com/dotenvx/dotenvx-ruby/compare/v4.0.0...main) -## [2.0.0](https://github.com/dotenvx/dotenvx-ruby/compare/v0.0.2...v2.0.0) +## [4.0.0](https://github.com/dotenvx/dotenvx-ruby/compare/v0.0.2...v4.0.0) - Load plaintext and encrypted dotenv files through `dotenvx-primitives`. - Bundle the Rust implementation as a Magnus native extension. diff --git a/Cargo.lock b/Cargo.lock index 07fff0a..b79714c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ [[package]] name = "dotenvx_native" -version = "2.0.0" +version = "4.0.0" dependencies = [ "dotenvx-primitives", "magnus", diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b194d2c..eab723c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -68,8 +68,8 @@ long-lived API-key secret is needed. After updating both version sources and the changelog: ```sh -git tag v2.0.0 -git push origin v2.0.0 +git tag v4.0.0 +git push origin v4.0.0 ``` The tag must match `Dotenvx::VERSION`. CI verifies this before packaging. diff --git a/Gemfile.lock b/Gemfile.lock index 5b4adb0..03840b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ PATH remote: . specs: - dotenvx (2.0.0) + dotenvx (4.0.0) rb_sys (~> 0.9) - dotenvx-rails (2.0.0) - dotenvx (= 2.0.0) + dotenvx-rails (4.0.0) + dotenvx (= 4.0.0) railties (>= 5.0) GEM diff --git a/ext/dotenvx/Cargo.toml b/ext/dotenvx/Cargo.toml index d786196..6e5a7f0 100644 --- a/ext/dotenvx/Cargo.toml +++ b/ext/dotenvx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dotenvx_native" -version = "2.0.0" +version = "4.0.0" edition = "2021" rust-version = "1.83" publish = false diff --git a/lib/dotenvx/version.rb b/lib/dotenvx/version.rb index d20dab2..5ddaab0 100644 --- a/lib/dotenvx/version.rb +++ b/lib/dotenvx/version.rb @@ -1,3 +1,3 @@ module Dotenvx - VERSION = "2.0.0" + VERSION = "4.0.0" end