fujin-cli 0.22.2__tar.gz → 0.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.github/workflows/publish.yml +3 -0
  2. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/CHANGELOG.md +17 -0
  3. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/PKG-INFO +1 -1
  4. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-1password/pyproject.toml +2 -2
  5. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-bitwarden/README.md +3 -1
  6. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-bitwarden/pyproject.toml +2 -2
  7. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/__init__.py +4 -3
  8. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-doppler/pyproject.toml +2 -2
  9. fujin_cli-0.24.0/plugins/fujin-secrets-env/README.md +86 -0
  10. fujin_cli-0.24.0/plugins/fujin-secrets-env/pyproject.toml +33 -0
  11. fujin_cli-0.24.0/plugins/fujin-secrets-env/src/fujin_secrets_env/__init__.py +95 -0
  12. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/pyproject.toml +4 -2
  13. fujin_cli-0.24.0/src/fujin/__init__.py +1 -0
  14. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/_installer.py +0 -8
  15. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/deploy.py +49 -4
  16. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/config.py +1 -1
  17. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/uv.lock +20 -4
  18. fujin_cli-0.22.2/src/fujin/__init__.py +0 -1
  19. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.github/FUNDING.yml +0 -0
  20. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.github/workflows/test.yml +0 -0
  21. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.gitignore +0 -0
  22. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.pre-commit-config.yaml +0 -0
  23. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/.readthedocs.yaml +0 -0
  24. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/CLAUDE.md +0 -0
  25. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/LICENSE.txt +0 -0
  26. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/README.md +0 -0
  27. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/Vagrantfile +0 -0
  28. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-cat-help.png +0 -0
  29. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-exec-help.png +0 -0
  30. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-help.png +0 -0
  31. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-logs-help.png +0 -0
  32. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-restart-help.png +0 -0
  33. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-scale-help.png +0 -0
  34. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-shell-help.png +0 -0
  35. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-start-help.png +0 -0
  36. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-status-help.png +0 -0
  37. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/app-stop-help.png +0 -0
  38. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/audit-help.png +0 -0
  39. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/deploy-help.png +0 -0
  40. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/down-help.png +0 -0
  41. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/fa-help.png +0 -0
  42. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/fujin-help.png +0 -0
  43. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/init-help.png +0 -0
  44. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/migrate-help.png +0 -0
  45. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/new-help.png +0 -0
  46. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/prune-help.png +0 -0
  47. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/rollback-help.png +0 -0
  48. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-bootstrap-help.png +0 -0
  49. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-create-user-help.png +0 -0
  50. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-exec-help.png +0 -0
  51. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-help.png +0 -0
  52. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-setup-ssh-help.png +0 -0
  53. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/server-status-help.png +0 -0
  54. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/_static/images/help/up-help.png +0 -0
  55. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/changelog.rst +0 -0
  56. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/app.rst +0 -0
  57. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/audit.rst +0 -0
  58. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/deploy.rst +0 -0
  59. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/down.rst +0 -0
  60. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/index.rst +0 -0
  61. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/init.rst +0 -0
  62. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/migrate.rst +0 -0
  63. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/new.rst +0 -0
  64. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/prune.rst +0 -0
  65. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/rollback.rst +0 -0
  66. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/server.rst +0 -0
  67. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/commands/up.rst +0 -0
  68. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/conf.py +0 -0
  69. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/configuration.rst +0 -0
  70. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/guides/django-complete.rst +0 -0
  71. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/guides/index.rst +0 -0
  72. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/guides/templates.rst +0 -0
  73. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/howtos/binary.rst +0 -0
  74. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/howtos/django.rst +0 -0
  75. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/howtos/index.rst +0 -0
  76. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/index.rst +0 -0
  77. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/installation.rst +0 -0
  78. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/integrations.rst +0 -0
  79. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/requirements.txt +0 -0
  80. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/secrets.rst +0 -0
  81. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/docs/troubleshooting.rst +0 -0
  82. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/.fujin/Caddyfile +0 -0
  83. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/.fujin/systemd/health.service +0 -0
  84. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/.fujin/systemd/health.timer +0 -0
  85. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/.fujin/systemd/web.service +0 -0
  86. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/.fujin/systemd/worker@.service +0 -0
  87. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/README.md +0 -0
  88. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
  89. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
  90. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
  91. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/management/commands/health.py +0 -0
  92. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/settings.py +0 -0
  93. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/urls.py +0 -0
  94. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
  95. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/fujin.toml +0 -0
  96. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/manage.py +0 -0
  97. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/pyproject.toml +0 -0
  98. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/examples/django/bookstore/requirements.txt +0 -0
  99. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/justfile +0 -0
  100. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-1password/README.md +0 -0
  101. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/__init__.py +0 -0
  102. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/py.typed +0 -0
  103. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/py.typed +0 -0
  104. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-doppler/README.md +0 -0
  105. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/__init__.py +0 -0
  106. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/py.typed +0 -0
  107. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/__main__.py +0 -0
  108. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/audit.py +0 -0
  109. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/caddy.py +0 -0
  110. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/__init__.py +0 -0
  111. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/_base.py +0 -0
  112. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/app.py +0 -0
  113. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/audit.py +0 -0
  114. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/down.py +0 -0
  115. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/init.py +0 -0
  116. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/migrate.py +0 -0
  117. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/new.py +0 -0
  118. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/prune.py +0 -0
  119. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/rollback.py +0 -0
  120. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/server.py +0 -0
  121. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/showenv.py +0 -0
  122. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/commands/up.py +0 -0
  123. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/connection.py +0 -0
  124. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/discovery.py +0 -0
  125. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/errors.py +0 -0
  126. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/fa.py +0 -0
  127. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/formatting.py +0 -0
  128. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/secrets.py +0 -0
  129. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/src/fujin/templates.py +0 -0
  130. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/__init__.py +0 -0
  131. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/conftest.py +0 -0
  132. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/Dockerfile +0 -0
  133. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/__init__.py +0 -0
  134. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/conftest.py +0 -0
  135. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/helpers.py +0 -0
  136. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/test_app_management.py +0 -0
  137. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/test_full_deploy.py +0 -0
  138. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/test_installation.py +0 -0
  139. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/integration/test_server_bootstrap.py +0 -0
  140. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_app.py +0 -0
  141. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_audit.py +0 -0
  142. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_caddy_domain.py +0 -0
  143. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_config.py +0 -0
  144. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_connection.py +0 -0
  145. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_deploy.py +0 -0
  146. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_discovery.py +0 -0
  147. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_down.py +0 -0
  148. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_init.py +0 -0
  149. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_new.py +0 -0
  150. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_prune.py +0 -0
  151. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_rollback.py +0 -0
  152. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_scale.py +0 -0
  153. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_secrets.py +0 -0
  154. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_server.py +0 -0
  155. {fujin_cli-0.22.2 → fujin_cli-0.24.0}/tests/test_up.py +0 -0
@@ -35,6 +35,7 @@ jobs:
35
35
  - bitwarden
36
36
  - 1password
37
37
  - doppler
38
+ - env
38
39
  steps:
39
40
  - name: Checkout code
40
41
  uses: actions/checkout@v4
@@ -169,6 +170,8 @@ jobs:
169
170
  artifact: wheels-plugin-1password
170
171
  - name: fujin-secrets-doppler
171
172
  artifact: wheels-plugin-doppler
173
+ - name: fujin-secrets-env
174
+ artifact: wheels-plugin-env
172
175
  steps:
173
176
  - name: Download artifacts for ${{ matrix.package.name }}
174
177
  uses: actions/download-artifact@v4
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.24.0] - 2026-03-21
8
+
9
+ ### 🚀 Features
10
+
11
+ - Add env secrets adapter and refactor adapter config
12
+
13
+ ### ⚙️ Miscellaneous Tasks
14
+
15
+ - Add fujin-secrets-env to publish workflow
16
+
17
+ ## [0.23.0] - 2026-03-21
18
+
19
+ ### 🚀 Features
20
+
21
+ - Stream .env directly to server instead of bundling
22
+ - Add --bundle-dir option to preserve deployment bundle
23
+
7
24
  ## [0.22.2] - 2026-03-21
8
25
 
9
26
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fujin-cli
3
- Version: 0.22.2
3
+ Version: 0.24.0
4
4
  Summary: Get your project up and running in a few minutes on your own vps.
5
5
  Project-URL: Documentation, https://github.com/Tobi-De/fujin#readme
6
6
  Project-URL: Issues, https://github.com/Tobi-De/fujin/issues
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-1password"
7
- version = "0.22.2"
7
+ version = "0.24.0"
8
8
  description = "1Password secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.22.2",
23
+ "fujin-cli>=0.24",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -27,12 +27,14 @@ Add the following to your `fujin.toml` file:
27
27
  ```toml
28
28
  [secrets]
29
29
  adapter = "bitwarden"
30
+
31
+ [secrets.options]
30
32
  password_env = "BW_PASSWORD"
31
33
  ```
32
34
 
33
35
  To unlock the Bitwarden vault, the password is required. Set the `BW_PASSWORD` environment variable in your shell. When Fujin signs in, it will always sync the vault first.
34
36
 
35
- Alternatively, you can set the `BW_SESSION` environment variable. If `BW_SESSION` is present, Fujin will use it directly without signing in or syncing the vault. In this case, the `password_env` configuration is not required.
37
+ Alternatively, you can set the `BW_SESSION` environment variable. If `BW_SESSION` is present, Fujin will use it directly without signing in or syncing the vault. In this case, the `options.password_env` configuration is not required.
36
38
 
37
39
  ## Usage
38
40
 
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-bitwarden"
7
- version = "0.22.2"
7
+ version = "0.24.0"
8
8
  description = "Bitwarden secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.22.2",
23
+ "fujin-cli>=0.24",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -54,13 +54,14 @@ def bitwarden(env_content: str, secret_config: SecretConfig) -> str:
54
54
  # Authenticate with Bitwarden
55
55
  session = os.getenv("BW_SESSION")
56
56
  if not session:
57
- if not secret_config.password_env:
57
+ password_env = secret_config.options.get("password_env")
58
+ if not password_env:
58
59
  raise SecretResolutionError(
59
- "You need to set the password_env to use the bitwarden adapter "
60
+ "You need to set options.password_env to use the bitwarden adapter "
60
61
  "or set the BW_SESSION environment variable",
61
62
  adapter="bitwarden",
62
63
  )
63
- session = _signin(secret_config.password_env)
64
+ session = _signin(password_env)
64
65
 
65
66
  # Resolve secrets concurrently
66
67
  resolved_secrets = {}
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
4
4
 
5
5
  [project]
6
6
  name = "fujin-secrets-doppler"
7
- version = "0.22.2"
7
+ version = "0.24.0"
8
8
  description = "Doppler secret adapter for Fujin"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.14",
21
21
  ]
22
22
  dependencies = [
23
- "fujin-cli>=0.22.2",
23
+ "fujin-cli>=0.24",
24
24
  "python-dotenv>=1.0.1",
25
25
  ]
26
26
 
@@ -0,0 +1,86 @@
1
+ # Fujin Secrets - Environment Variable
2
+
3
+ Environment variable secret adapter for Fujin deployment tool. Reads secrets from a JSON-formatted environment variable, making it ideal for CI systems like GitHub Actions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install fujin-secrets-env
9
+ ```
10
+
11
+ Or with uv:
12
+
13
+ ```bash
14
+ uv pip install fujin-secrets-env
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ Add the following to your `fujin.toml` file:
20
+
21
+ ```toml
22
+ [secrets]
23
+ adapter = "env"
24
+
25
+ [secrets.options]
26
+ source = "FUJIN_SECRETS"
27
+ ```
28
+
29
+ The `source` option specifies the name of the environment variable containing the JSON-formatted secrets.
30
+
31
+ ## Usage
32
+
33
+ ### GitHub Actions
34
+
35
+ In your workflow file, pass all secrets as JSON using `toJSON(secrets)`:
36
+
37
+ ```yaml
38
+ - name: Deploy
39
+ run: uvx --from fujin-cli --with fujin-secrets-env fujin deploy
40
+ env:
41
+ FUJIN_SECRETS: ${{ toJSON(secrets) }}
42
+ ```
43
+
44
+ ### Environment File
45
+
46
+ In your environment configuration (via `env` in `fujin.toml`), prefix secret values with `$`:
47
+
48
+ ```env
49
+ DEBUG=False
50
+ SECRET_KEY=$SECRET_KEY
51
+ DATABASE_URL=$DATABASE_URL
52
+ AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
53
+ ```
54
+
55
+ The `$` prefix indicates to Fujin that the value should be resolved from the secrets source.
56
+
57
+ ## How it Works
58
+
59
+ The adapter:
60
+ 1. Reads the JSON string from the configured environment variable
61
+ 2. Parses the JSON into a dictionary
62
+ 3. For each secret reference (prefixed with `$`), looks up the value in the parsed JSON
63
+ 4. Returns the resolved environment variables
64
+
65
+ ## Example
66
+
67
+ Given this GitHub Actions secret setup:
68
+ - `SECRET_KEY`: `my-secret-key`
69
+ - `DATABASE_URL`: `postgres://...`
70
+
71
+ And this fujin.toml env configuration:
72
+ ```toml
73
+ [[hosts]]
74
+ env = """
75
+ DEBUG=False
76
+ SECRET_KEY=$SECRET_KEY
77
+ DATABASE_URL=$DATABASE_URL
78
+ """
79
+ ```
80
+
81
+ The adapter will resolve `$SECRET_KEY` and `$DATABASE_URL` from the JSON passed via `FUJIN_SECRETS`.
82
+
83
+ ## Related
84
+
85
+ - [Fujin Documentation](https://github.com/Tobi-De/fujin)
86
+ - [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = [ "uv-build>=0.9.18,<0.10" ]
4
+
5
+ [project]
6
+ name = "fujin-secrets-env"
7
+ version = "0.23.0"
8
+ description = "Environment variable secret adapter for Fujin"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Tobi", email = "tobidegnon@proton.me" },
12
+ ]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "fujin-cli>=0.23",
24
+ "python-dotenv>=1.0.1",
25
+ ]
26
+
27
+ urls.Homepage = "https://github.com/Tobi-De/fujin"
28
+ urls.Issues = "https://github.com/Tobi-De/fujin/issues"
29
+ urls.Source = "https://github.com/Tobi-De/fujin"
30
+ entry-points."fujin.secrets".env = "fujin_secrets_env:env"
31
+
32
+ [tool.uv.sources]
33
+ fujin-cli = { workspace = true }
@@ -0,0 +1,95 @@
1
+ """Environment variable secret adapter for Fujin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from contextlib import closing
8
+ from io import StringIO
9
+
10
+ from dotenv import dotenv_values
11
+
12
+ from fujin.config import SecretConfig
13
+ from fujin.errors import SecretResolutionError
14
+
15
+
16
+ def env(env_content: str, secret_config: SecretConfig) -> str:
17
+ """Environment variable secret adapter.
18
+
19
+ Reads secrets from a JSON-formatted environment variable. Useful for CI
20
+ systems like GitHub Actions where secrets can be passed as JSON.
21
+
22
+ Configuration:
23
+ [secrets]
24
+ adapter = "env"
25
+ source = "FUJIN_SECRETS" # Name of env var containing JSON secrets
26
+
27
+ Example GitHub Actions usage:
28
+ env:
29
+ FUJIN_SECRETS: ${{ toJSON(secrets) }}
30
+
31
+ Args:
32
+ env_content: Raw environment file content
33
+ secret_config: Secret configuration with adapter settings
34
+
35
+ Returns:
36
+ Resolved environment content with secrets replaced
37
+
38
+ Raises:
39
+ SecretResolutionError: If source env var not set or invalid JSON
40
+ """
41
+ source = secret_config.options.get("source")
42
+ if not source:
43
+ raise SecretResolutionError(
44
+ "The 'options.source' parameter is required for the env adapter. "
45
+ "Set it to the name of the environment variable containing secrets JSON.",
46
+ adapter="env",
47
+ )
48
+
49
+ # Read JSON from source env var
50
+ secrets_json = os.getenv(source)
51
+ if not secrets_json:
52
+ raise SecretResolutionError(
53
+ f"Environment variable '{source}' is not set or empty.",
54
+ adapter="env",
55
+ )
56
+
57
+ try:
58
+ secrets = json.loads(secrets_json)
59
+ except json.JSONDecodeError as e:
60
+ raise SecretResolutionError(
61
+ f"Invalid JSON in '{source}': {e}",
62
+ adapter="env",
63
+ ) from e
64
+
65
+ if not isinstance(secrets, dict):
66
+ raise SecretResolutionError(
67
+ f"'{source}' must contain a JSON object, got {type(secrets).__name__}",
68
+ adapter="env",
69
+ )
70
+
71
+ # Parse env file
72
+ with closing(StringIO(env_content)) as buffer:
73
+ env_dict = dotenv_values(stream=buffer)
74
+
75
+ # Identify secrets (values starting with $)
76
+ secret_refs = {
77
+ key: value[1:] # Strip the $ prefix
78
+ for key, value in env_dict.items()
79
+ if value and value.startswith("$")
80
+ }
81
+
82
+ if not secret_refs:
83
+ return env_content
84
+
85
+ # Resolve secrets
86
+ for key, secret_name in secret_refs.items():
87
+ if secret_name not in secrets:
88
+ raise SecretResolutionError(
89
+ f"Secret '{secret_name}' not found in '{source}'",
90
+ adapter="env",
91
+ key=key,
92
+ )
93
+ env_dict[key] = secrets[secret_name]
94
+
95
+ return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
@@ -5,7 +5,7 @@ requires = [ "hatchling" ]
5
5
 
6
6
  [project]
7
7
  name = "fujin-cli"
8
- version = "0.22.2"
8
+ version = "0.24.0"
9
9
  description = "Get your project up and running in a few minutes on your own vps."
10
10
  readme = "README.md"
11
11
  keywords = [
@@ -74,10 +74,12 @@ members = [
74
74
  "plugins/fujin-secrets-bitwarden",
75
75
  "plugins/fujin-secrets-1password",
76
76
  "plugins/fujin-secrets-doppler",
77
+ "plugins/fujin-secrets-env",
77
78
  ]
78
79
 
79
80
  [tool.uv.sources]
80
81
  fujin-secrets-bitwarden = { workspace = true }
82
+ fujin-secrets-env = { workspace = true }
81
83
 
82
84
  [tool.ruff]
83
85
  # Assume Python {{ python_version }}
@@ -149,7 +151,7 @@ markers = [
149
151
  ]
150
152
 
151
153
  [tool.bumpversion]
152
- current_version = "0.22.2"
154
+ current_version = "0.24.0"
153
155
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
154
156
  serialize = [ "{major}.{minor}.{patch}" ]
155
157
  search = "{current_version}"
@@ -0,0 +1 @@
1
+ __version__ = "0.24.0"
@@ -135,13 +135,6 @@ def install(
135
135
  install_dir = app_dir / ".install"
136
136
  install_dir.mkdir(exist_ok=True)
137
137
 
138
- # Move .env file to .install/
139
- env_file = bundle_dir / ".env"
140
- if env_file.exists():
141
- shutil.move(env_file, install_dir / ".env")
142
- env_file = install_dir / ".env"
143
- logger.debug("Moved .env to %s", env_file)
144
-
145
138
  # ==========================================================================
146
139
  # PHASE 2: INSTALLATION
147
140
  # ==========================================================================
@@ -221,7 +214,6 @@ export -f {config.app_name}
221
214
  run(f"chown -R {config.deploy_user}:{config.app_user} {install_dir}")
222
215
  # Make .install directory group-writable (deploy user can update, app user can read)
223
216
  install_dir.chmod(0o775)
224
- env_file.chmod(0o640)
225
217
 
226
218
  # .venv permissions: readable/executable by group, writable by owner
227
219
  # Use chmod -R with symbolic modes to traverse once instead of 3 find commands
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
5
  import logging
5
6
  import shlex
@@ -8,9 +9,10 @@ import hashlib
8
9
  import subprocess
9
10
  import tempfile
10
11
  import zipapp
12
+ from contextlib import contextmanager
11
13
  from dataclasses import dataclass
12
14
  from pathlib import Path
13
- from typing import Annotated
15
+ from typing import Annotated, Generator
14
16
 
15
17
  import cappa
16
18
  from rich.console import Console
@@ -61,6 +63,13 @@ class Deploy(BaseCommand):
61
63
  help="Disable automatic rollback on deployment failure",
62
64
  ),
63
65
  ] = False
66
+ bundle_dir: Annotated[
67
+ Path | None,
68
+ cappa.Arg(
69
+ long="--bundle-dir",
70
+ help="Directory to create bundle in (preserved after deploy, fails if exists)",
71
+ ),
72
+ ] = None
64
73
 
65
74
  def __call__(self):
66
75
  logger.debug("Starting deployment for %s", self.config.app_name)
@@ -124,7 +133,7 @@ class Deploy(BaseCommand):
124
133
  if not self.config.deployed_units:
125
134
  raise DeploymentError("No systemd units found, nothing to deploy")
126
135
 
127
- with tempfile.TemporaryDirectory() as tmpdir:
136
+ with self._bundle_directory() as tmpdir:
128
137
  self.output.info("Preparing deployment bundle...")
129
138
  zipapp_dir = Path(tmpdir) / "zipapp_source"
130
139
  zipapp_dir.mkdir()
@@ -162,10 +171,9 @@ class Deploy(BaseCommand):
162
171
  # Track unresolved variables across all files
163
172
  all_unresolved = set()
164
173
 
165
- # resolve and copy env file
174
+ # Resolve env file (uploaded separately, not bundled)
166
175
  resolved_env, unresolved = safe_format(parsed_env, **context)
167
176
  all_unresolved.update(unresolved)
168
- (zipapp_dir / ".env").write_text(resolved_env)
169
177
 
170
178
  logger.debug("Validating and resolving systemd units")
171
179
  systemd_dir = zipapp_dir / "systemd"
@@ -351,6 +359,23 @@ class Deploy(BaseCommand):
351
359
  conn.put(str(zipapp_path), remote_bundle_path_q, verify=True)
352
360
 
353
361
  self.output.success("Bundle uploaded successfully.")
362
+
363
+ # Write .env file directly (not bundled for security)
364
+ remote_env_path = f"{self.config.install_dir}/.env"
365
+ remote_env_path_q = shlex.quote(remote_env_path)
366
+ install_dir_q = shlex.quote(self.config.install_dir)
367
+ logger.debug("Writing .env to %s", remote_env_path)
368
+
369
+ # Use base64 encoding to safely transfer content with special chars
370
+ encoded_env = base64.b64encode(resolved_env.encode()).decode()
371
+ conn.run(
372
+ f"mkdir -p {install_dir_q} && "
373
+ f"echo {shlex.quote(encoded_env)} | base64 -d > {remote_env_path_q} && "
374
+ f"chmod 640 {remote_env_path_q} && "
375
+ f"chown {self.selected_host.user}:{self.config.app_user} {remote_env_path_q}",
376
+ hide=True,
377
+ )
378
+
354
379
  self.output.info("Executing remote installation...")
355
380
 
356
381
  rollback_ran = False
@@ -425,6 +450,26 @@ class Deploy(BaseCommand):
425
450
  url = f"https://{domain}"
426
451
  self.output.info(f"Application is available at: {url}")
427
452
 
453
+ @contextmanager
454
+ def _bundle_directory(self) -> Generator[Path, None, None]:
455
+ """Context manager for bundle directory.
456
+
457
+ If bundle_dir is specified, uses that directory (fails if exists).
458
+ Otherwise, creates a temporary directory that is cleaned up on exit.
459
+ """
460
+ if self.bundle_dir:
461
+ if self.bundle_dir.exists():
462
+ raise DeploymentError(
463
+ f"Bundle directory already exists: {self.bundle_dir}"
464
+ )
465
+ self.bundle_dir.mkdir(parents=True)
466
+ logger.debug("Using bundle directory: %s", self.bundle_dir)
467
+ yield self.bundle_dir
468
+ self.output.info(f"Bundle preserved in: {self.bundle_dir}")
469
+ else:
470
+ with tempfile.TemporaryDirectory() as tmpdir:
471
+ yield Path(tmpdir)
472
+
428
473
  def _show_deployment_summary(self, bundle_size: int, bundle_version: str):
429
474
  console = Console()
430
475
 
@@ -35,7 +35,7 @@ class InstallationMode(StrEnum):
35
35
 
36
36
  class SecretConfig(msgspec.Struct):
37
37
  adapter: str
38
- password_env: str | None = None
38
+ options: dict[str, str] = msgspec.field(default_factory=dict)
39
39
 
40
40
  def __post_init__(self):
41
41
  if not re.match(r"^[a-z0-9_-]+$", self.adapter):
@@ -15,6 +15,7 @@ members = [
15
15
  "fujin-secrets-1password",
16
16
  "fujin-secrets-bitwarden",
17
17
  "fujin-secrets-doppler",
18
+ "fujin-secrets-env",
18
19
  ]
19
20
 
20
21
  [[package]]
@@ -345,7 +346,7 @@ wheels = [
345
346
 
346
347
  [[package]]
347
348
  name = "fujin-cli"
348
- version = "0.22.0"
349
+ version = "0.23.0"
349
350
  source = { editable = "." }
350
351
  dependencies = [
351
352
  { name = "cappa" },
@@ -401,7 +402,7 @@ docs = [
401
402
 
402
403
  [[package]]
403
404
  name = "fujin-secrets-1password"
404
- version = "0.22.0"
405
+ version = "0.23.0"
405
406
  source = { editable = "plugins/fujin-secrets-1password" }
406
407
  dependencies = [
407
408
  { name = "fujin-cli" },
@@ -416,7 +417,7 @@ requires-dist = [
416
417
 
417
418
  [[package]]
418
419
  name = "fujin-secrets-bitwarden"
419
- version = "0.22.0"
420
+ version = "0.23.0"
420
421
  source = { editable = "plugins/fujin-secrets-bitwarden" }
421
422
  dependencies = [
422
423
  { name = "fujin-cli" },
@@ -431,7 +432,7 @@ requires-dist = [
431
432
 
432
433
  [[package]]
433
434
  name = "fujin-secrets-doppler"
434
- version = "0.22.0"
435
+ version = "0.23.0"
435
436
  source = { editable = "plugins/fujin-secrets-doppler" }
436
437
  dependencies = [
437
438
  { name = "fujin-cli" },
@@ -444,6 +445,21 @@ requires-dist = [
444
445
  { name = "python-dotenv", specifier = ">=1.0.1" },
445
446
  ]
446
447
 
448
+ [[package]]
449
+ name = "fujin-secrets-env"
450
+ version = "0.23.0"
451
+ source = { editable = "plugins/fujin-secrets-env" }
452
+ dependencies = [
453
+ { name = "fujin-cli" },
454
+ { name = "python-dotenv" },
455
+ ]
456
+
457
+ [package.metadata]
458
+ requires-dist = [
459
+ { name = "fujin-cli", editable = "." },
460
+ { name = "python-dotenv", specifier = ">=1.0.1" },
461
+ ]
462
+
447
463
  [[package]]
448
464
  name = "gunicorn"
449
465
  version = "25.1.0"
@@ -1 +0,0 @@
1
- __version__ = "0.22.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes