fujin-cli 0.25.2__tar.gz → 0.26.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.
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/CHANGELOG.md +11 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/PKG-INFO +1 -1
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/configuration.rst +71 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/justfile +1 -1
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-1password/pyproject.toml +2 -2
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-bitwarden/pyproject.toml +2 -2
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-doppler/pyproject.toml +2 -2
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-env/pyproject.toml +2 -2
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/pyproject.toml +2 -2
- fujin_cli-0.26.0/src/fujin/__init__.py +1 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/_installer.py +65 -1
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/deploy.py +123 -22
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/config.py +15 -3
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/connection.py +74 -61
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/uv.lock +5 -5
- fujin_cli-0.25.2/src/fujin/__init__.py +0 -1
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.github/FUNDING.yml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.github/workflows/publish.yml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.github/workflows/test.yml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.gitignore +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.pre-commit-config.yaml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/.readthedocs.yaml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/CLAUDE.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/LICENSE.txt +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/Vagrantfile +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-cat-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-exec-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-logs-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-restart-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-scale-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-shell-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-start-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-status-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/app-stop-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/audit-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/deploy-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/down-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/fa-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/fujin-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/init-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/migrate-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/new-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/prune-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/rollback-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-bootstrap-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-create-user-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-exec-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-setup-ssh-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/server-status-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/_static/images/help/up-help.png +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/changelog.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/app.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/audit.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/deploy.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/down.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/index.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/init.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/migrate.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/new.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/prune.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/rollback.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/server.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/commands/up.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/conf.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/guides/django-complete.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/guides/index.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/guides/templates.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/howtos/binary.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/howtos/django.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/howtos/index.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/index.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/installation.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/integrations.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/requirements.txt +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/secrets.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/docs/troubleshooting.rst +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/.fujin/Caddyfile +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/.fujin/systemd/health.service +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/.fujin/systemd/health.timer +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/.fujin/systemd/web.service +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/.fujin/systemd/worker@.service +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/management/commands/health.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/settings.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/urls.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/fujin.toml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/manage.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/pyproject.toml +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/examples/django/bookstore/requirements.txt +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-1password/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-1password/src/fujin_secrets_1password/py.typed +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-bitwarden/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-bitwarden/src/fujin_secrets_bitwarden/py.typed +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-doppler/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-doppler/src/fujin_secrets_doppler/py.typed +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-env/README.md +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/plugins/fujin-secrets-env/src/fujin_secrets_env/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/__main__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/audit.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/caddy.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/_base.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/app.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/audit.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/down.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/init.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/migrate.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/new.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/prune.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/rollback.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/server.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/showenv.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/commands/up.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/discovery.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/errors.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/fa.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/formatting.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/secrets.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/src/fujin/templates.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/conftest.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/Dockerfile +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/__init__.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/conftest.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/helpers.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/test_app_management.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/test_full_deploy.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/test_installation.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/integration/test_server_bootstrap.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_app.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_audit.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_caddy_domain.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_config.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_connection.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_deploy.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_discovery.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_down.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_init.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_new.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_prune.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_rollback.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_scale.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_secrets.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_server.py +0 -0
- {fujin_cli-0.25.2 → fujin_cli-0.26.0}/tests/test_up.py +0 -0
|
@@ -4,6 +4,17 @@ 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.26.0] - 2026-06-05
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- Hooks system
|
|
12
|
+
- Improve reliability
|
|
13
|
+
|
|
14
|
+
### ⚡ Performance
|
|
15
|
+
|
|
16
|
+
- Ssh and deploy units cache
|
|
17
|
+
|
|
7
18
|
## [0.25.2] - 2026-03-31
|
|
8
19
|
|
|
9
20
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fujin-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.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
|
|
@@ -177,6 +177,77 @@ For static files (Django example):
|
|
|
177
177
|
|
|
178
178
|
The ``{app_dir}`` variable is substituted during deployment.
|
|
179
179
|
|
|
180
|
+
Hooks
|
|
181
|
+
-----
|
|
182
|
+
|
|
183
|
+
Deployment hooks let you run custom commands at specific points during the deploy and rollback lifecycle. They are defined in the ``[hooks]`` section of ``fujin.toml``.
|
|
184
|
+
|
|
185
|
+
Hook Phases
|
|
186
|
+
~~~~~~~~~~~
|
|
187
|
+
|
|
188
|
+
**pre-install**
|
|
189
|
+
Runs before the installer, as the deploy (SSH) user. Use for infrastructure setup like installing packages or creating databases. Has access to environment variables from ``.env``. **Failure is fatal** — the deploy stops and rolls back.
|
|
190
|
+
|
|
191
|
+
.. code-block:: toml
|
|
192
|
+
:caption: fujin.toml
|
|
193
|
+
|
|
194
|
+
[hooks]
|
|
195
|
+
pre_install = [
|
|
196
|
+
"sudo apt-get install -y postgresql",
|
|
197
|
+
"sudo -u postgres createdb myapp",
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
**post-install**
|
|
201
|
+
Runs after the package is installed but before systemd services are started. Executes as ``app_user`` via ``sudo -u``, within the app's environment (``.appenv`` sourced, so the app binary is on ``PATH``). Use for database migrations. **Failure is fatal** — the deploy stops.
|
|
202
|
+
|
|
203
|
+
.. code-block:: toml
|
|
204
|
+
:caption: fujin.toml
|
|
205
|
+
|
|
206
|
+
[hooks]
|
|
207
|
+
post_install = [
|
|
208
|
+
"python manage.py migrate --noinput",
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
**post-start**
|
|
212
|
+
Runs after services are confirmed running, before Caddy configuration. Executes as ``app_user`` within the app environment. Use for static file collection, cache warming, or health checks. **Failure is non-fatal** — services are already running, a warning is logged but the deploy succeeds.
|
|
213
|
+
|
|
214
|
+
.. code-block:: toml
|
|
215
|
+
:caption: fujin.toml
|
|
216
|
+
|
|
217
|
+
[hooks]
|
|
218
|
+
post_start = [
|
|
219
|
+
"python manage.py collectstatic --noinput",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
Available Variables
|
|
223
|
+
~~~~~~~~~~~~~~~~~~
|
|
224
|
+
|
|
225
|
+
Hook commands support the same template variables as systemd unit files:
|
|
226
|
+
|
|
227
|
+
- ``{app_name}`` - Application name
|
|
228
|
+
- ``{app_dir}`` - Full path to application directory (``/opt/fujin/{app_name}``)
|
|
229
|
+
- ``{app_user}`` - User running the app
|
|
230
|
+
- ``{version}`` - Current version being deployed
|
|
231
|
+
- ``{install_dir}`` - Path to ``.install`` directory
|
|
232
|
+
|
|
233
|
+
Complete Example
|
|
234
|
+
~~~~~~~~~~~~~~~~
|
|
235
|
+
|
|
236
|
+
.. code-block:: toml
|
|
237
|
+
:caption: fujin.toml
|
|
238
|
+
|
|
239
|
+
[hooks]
|
|
240
|
+
pre_install = [
|
|
241
|
+
"sudo -u postgres createdb myapp",
|
|
242
|
+
]
|
|
243
|
+
post_install = [
|
|
244
|
+
"python manage.py migrate --noinput",
|
|
245
|
+
]
|
|
246
|
+
post_start = [
|
|
247
|
+
"python manage.py collectstatic --noinput",
|
|
248
|
+
"python manage.py clearsessions",
|
|
249
|
+
]
|
|
250
|
+
|
|
180
251
|
Host Configuration
|
|
181
252
|
-------------------
|
|
182
253
|
|
|
@@ -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.
|
|
7
|
+
version = "0.26.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.
|
|
23
|
+
"fujin-cli>=0.26",
|
|
24
24
|
"python-dotenv>=1.0.1",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -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.
|
|
7
|
+
version = "0.26.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.
|
|
23
|
+
"fujin-cli>=0.26",
|
|
24
24
|
"python-dotenv>=1.0.1",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -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.
|
|
7
|
+
version = "0.26.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.
|
|
23
|
+
"fujin-cli>=0.26",
|
|
24
24
|
"python-dotenv>=1.0.1",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.9.18,<0.10" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fujin-secrets-env"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.26.0"
|
|
8
8
|
description = "Environment variable 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.
|
|
23
|
+
"fujin-cli>=0.26",
|
|
24
24
|
"python-dotenv>=1.0.1",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -5,7 +5,7 @@ requires = [ "hatchling" ]
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "fujin-cli"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.26.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 = [
|
|
@@ -151,7 +151,7 @@ markers = [
|
|
|
151
151
|
]
|
|
152
152
|
|
|
153
153
|
[tool.bumpversion]
|
|
154
|
-
current_version = "0.
|
|
154
|
+
current_version = "0.26.0"
|
|
155
155
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
156
156
|
serialize = [ "{major}.{minor}.{patch}" ]
|
|
157
157
|
search = "{current_version}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.26.0"
|
|
@@ -13,7 +13,7 @@ import sys
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import time
|
|
15
15
|
import zipfile
|
|
16
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
17
|
from itertools import chain
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Literal, TypedDict
|
|
@@ -62,6 +62,7 @@ class InstallConfig:
|
|
|
62
62
|
caddy_config_path: str
|
|
63
63
|
app_bin: str
|
|
64
64
|
deployed_units: list[DeployedUnit]
|
|
65
|
+
hooks: dict[str, list[str]] = field(default_factory=dict)
|
|
65
66
|
|
|
66
67
|
@property
|
|
67
68
|
def uv_path(self) -> str:
|
|
@@ -102,6 +103,30 @@ def _setup_logging(verbose: int) -> None:
|
|
|
102
103
|
logger.setLevel(level)
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
def _run_hooks(config: InstallConfig, phase: str, *, fatal: bool = True) -> None:
|
|
107
|
+
commands = config.hooks.get(phase, [])
|
|
108
|
+
if not commands:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
logger.info("Running %s hooks...", phase)
|
|
112
|
+
install_dir = f"{config.app_dir}/.install"
|
|
113
|
+
for cmd in commands:
|
|
114
|
+
logger.info(" [%s] %s", phase, cmd)
|
|
115
|
+
full_cmd = f"sudo -u {shlex.quote(config.app_user)} bash -c " + shlex.quote(
|
|
116
|
+
f"source {install_dir}/.appenv 2>/dev/null ; {cmd}"
|
|
117
|
+
)
|
|
118
|
+
try:
|
|
119
|
+
run(full_cmd, check=True, timeout=300)
|
|
120
|
+
except subprocess.TimeoutExpired:
|
|
121
|
+
if fatal:
|
|
122
|
+
raise
|
|
123
|
+
logger.warning("[%s] Hook timed out: %s", phase, cmd)
|
|
124
|
+
except subprocess.CalledProcessError as e:
|
|
125
|
+
if fatal:
|
|
126
|
+
raise
|
|
127
|
+
logger.warning("[%s] Hook failed (exit %d): %s", phase, e.returncode, cmd)
|
|
128
|
+
|
|
129
|
+
|
|
105
130
|
def install(
|
|
106
131
|
config: InstallConfig, bundle_dir: Path, *, full_restart: bool = False
|
|
107
132
|
) -> None:
|
|
@@ -226,6 +251,11 @@ export -f {config.app_name}
|
|
|
226
251
|
run(f"chown {config.deploy_user}:{config.app_user} {app_dir}")
|
|
227
252
|
app_dir.chmod(0o775)
|
|
228
253
|
|
|
254
|
+
# ==========================================================================
|
|
255
|
+
# PHASE 2.5: POST-INSTALL HOOKS
|
|
256
|
+
# ==========================================================================
|
|
257
|
+
_run_hooks(config, "post_install", fatal=True)
|
|
258
|
+
|
|
229
259
|
# ==========================================================================
|
|
230
260
|
# PHASE 3: CONFIGURING SYSTEMD SERVICES
|
|
231
261
|
# ==========================================================================
|
|
@@ -350,6 +380,35 @@ export -f {config.app_name}
|
|
|
350
380
|
if unit["template_timer_name"]:
|
|
351
381
|
active_units.append(unit["template_timer_name"])
|
|
352
382
|
|
|
383
|
+
# Validate all unit files before enabling/starting (catch errors early)
|
|
384
|
+
logger.info("Validating systemd unit files...")
|
|
385
|
+
validation_issues = 0
|
|
386
|
+
for unit in config.deployed_units:
|
|
387
|
+
unit_path = SYSTEMD_SYSTEM_DIR / unit["template_service_name"]
|
|
388
|
+
if not unit_path.exists():
|
|
389
|
+
continue
|
|
390
|
+
result = subprocess.run(
|
|
391
|
+
f"systemd-analyze verify {shlex.quote(str(unit_path))}",
|
|
392
|
+
shell=True,
|
|
393
|
+
capture_output=True,
|
|
394
|
+
text=True,
|
|
395
|
+
)
|
|
396
|
+
if result.returncode != 0 or result.stderr.strip():
|
|
397
|
+
validation_issues += 1
|
|
398
|
+
logger.warning(
|
|
399
|
+
"Validation issues in %s (exit code %d):",
|
|
400
|
+
unit_path.name,
|
|
401
|
+
result.returncode,
|
|
402
|
+
)
|
|
403
|
+
for line in result.stderr.splitlines():
|
|
404
|
+
logger.warning(" %s", line)
|
|
405
|
+
|
|
406
|
+
if validation_issues:
|
|
407
|
+
logger.warning(
|
|
408
|
+
"%d unit(s) have validation warnings - review above before continuing",
|
|
409
|
+
validation_issues,
|
|
410
|
+
)
|
|
411
|
+
|
|
353
412
|
units_str = " ".join(active_units)
|
|
354
413
|
run(
|
|
355
414
|
f"systemctl daemon-reload && systemctl enable {units_str}",
|
|
@@ -423,6 +482,11 @@ export -f {config.app_name}
|
|
|
423
482
|
)
|
|
424
483
|
sys.exit(EXIT_SERVICE_START_FAILED)
|
|
425
484
|
|
|
485
|
+
# ==========================================================================
|
|
486
|
+
# PHASE 3.5: POST-START HOOKS
|
|
487
|
+
# ==========================================================================
|
|
488
|
+
_run_hooks(config, "post_start", fatal=False)
|
|
489
|
+
|
|
426
490
|
# ==========================================================================
|
|
427
491
|
# PHASE 4: CADDY CONFIGURATION
|
|
428
492
|
# ==========================================================================
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import json
|
|
5
4
|
import logging
|
|
6
5
|
import shlex
|
|
@@ -25,6 +24,7 @@ from fujin.audit import log_operation
|
|
|
25
24
|
from fujin.commands import BaseCommand
|
|
26
25
|
from fujin.commands.rollback import Rollback
|
|
27
26
|
from fujin.config import get_git_short_hash
|
|
27
|
+
from fujin.connection import SSH2Connection
|
|
28
28
|
from fujin.errors import (
|
|
29
29
|
BuildError,
|
|
30
30
|
CommandError,
|
|
@@ -94,7 +94,26 @@ class Deploy(BaseCommand):
|
|
|
94
94
|
try:
|
|
95
95
|
logger.debug("Build command: %s", self.config.build_command)
|
|
96
96
|
self.output.info(f"Building application ...")
|
|
97
|
-
subprocess.run(
|
|
97
|
+
subprocess.run(
|
|
98
|
+
self.config.build_command,
|
|
99
|
+
check=True,
|
|
100
|
+
shell=True,
|
|
101
|
+
timeout=self.config.build_timeout,
|
|
102
|
+
)
|
|
103
|
+
except subprocess.TimeoutExpired:
|
|
104
|
+
self.output.error(
|
|
105
|
+
f"Build command timed out after {self.config.build_timeout}s"
|
|
106
|
+
)
|
|
107
|
+
self.output.info(
|
|
108
|
+
f"Command: {self.config.build_command}\n\n"
|
|
109
|
+
"Troubleshooting:\n"
|
|
110
|
+
" - The build is taking too long; increase build_timeout in fujin.toml\n"
|
|
111
|
+
" - Check for infinite loops or hung subprocesses in your build\n"
|
|
112
|
+
" - Try running the build command manually to see its progress"
|
|
113
|
+
)
|
|
114
|
+
raise BuildError(
|
|
115
|
+
"Build timed out", command=self.config.build_command
|
|
116
|
+
) from None
|
|
98
117
|
except subprocess.CalledProcessError as e:
|
|
99
118
|
self.output.error(f"Build command failed with exit code {e.returncode}")
|
|
100
119
|
self.output.info(
|
|
@@ -274,6 +293,23 @@ class Deploy(BaseCommand):
|
|
|
274
293
|
all_unresolved.update(unresolved)
|
|
275
294
|
(zipapp_dir / "Caddyfile").write_text(resolved_caddyfile)
|
|
276
295
|
|
|
296
|
+
# Resolve hook commands
|
|
297
|
+
resolved_hooks: dict[str, list[str]] = {}
|
|
298
|
+
if self.config.hooks_config:
|
|
299
|
+
for phase in (
|
|
300
|
+
"pre_install",
|
|
301
|
+
"post_install",
|
|
302
|
+
"post_start",
|
|
303
|
+
):
|
|
304
|
+
commands = getattr(self.config.hooks_config, phase)
|
|
305
|
+
resolved_commands = []
|
|
306
|
+
for cmd in commands:
|
|
307
|
+
resolved, unresolved = safe_format(cmd, **context)
|
|
308
|
+
all_unresolved.update(unresolved)
|
|
309
|
+
resolved_commands.append(resolved)
|
|
310
|
+
if resolved_commands:
|
|
311
|
+
resolved_hooks[phase] = resolved_commands
|
|
312
|
+
|
|
277
313
|
if all_unresolved:
|
|
278
314
|
self.output.warning(
|
|
279
315
|
f"Found unresolved variables in configuration files: {', '.join(sorted(all_unresolved))}\n"
|
|
@@ -295,6 +331,7 @@ class Deploy(BaseCommand):
|
|
|
295
331
|
"caddy_config_path": self.config.caddy_config_path,
|
|
296
332
|
"app_bin": self.config.app_name, # Just the binary name, not full path
|
|
297
333
|
"deployed_units": deployed_units_data,
|
|
334
|
+
"hooks": resolved_hooks,
|
|
298
335
|
}
|
|
299
336
|
|
|
300
337
|
# Write config without indent for smaller size
|
|
@@ -325,7 +362,7 @@ class Deploy(BaseCommand):
|
|
|
325
362
|
min_rsync_size = 30 * 1024 * 1024
|
|
326
363
|
|
|
327
364
|
# Upload and Execute
|
|
328
|
-
with self.
|
|
365
|
+
with self._deploy_session() as conn:
|
|
329
366
|
# Check rsync availability while creating the directory (single round trip)
|
|
330
367
|
# Only check if bundle is large enough to benefit from rsync
|
|
331
368
|
if bundle_size >= min_rsync_size:
|
|
@@ -367,43 +404,67 @@ class Deploy(BaseCommand):
|
|
|
367
404
|
|
|
368
405
|
self.output.success("Bundle uploaded successfully.")
|
|
369
406
|
|
|
407
|
+
self.output.info("Executing remote installation...")
|
|
408
|
+
|
|
370
409
|
# Write .env file directly (not bundled for security)
|
|
371
410
|
remote_env_path = f"{self.config.install_dir}/.env"
|
|
372
411
|
remote_env_path_q = shlex.quote(remote_env_path)
|
|
373
412
|
install_dir_q = shlex.quote(self.config.install_dir)
|
|
413
|
+
remote_env_backup_q = shlex.quote(f"{remote_env_path}.bak")
|
|
374
414
|
logger.debug("Writing .env to %s", remote_env_path)
|
|
375
415
|
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
# chown may fail on first deploy if app_user doesn't exist yet
|
|
379
|
-
# (installer creates it), so use || true to make it non-fatal
|
|
380
|
-
write_env_cmd = (
|
|
416
|
+
# Backup existing .env (ensure install dir exists first)
|
|
417
|
+
conn.run(
|
|
381
418
|
f"mkdir -p {install_dir_q} && "
|
|
382
|
-
f"
|
|
383
|
-
|
|
384
|
-
f"(chown {self.selected_host.user}:{self.config.app_user} {remote_env_path_q} || true)"
|
|
419
|
+
f"cp {remote_env_path_q} {remote_env_backup_q} 2>/dev/null || true",
|
|
420
|
+
hide=True,
|
|
385
421
|
)
|
|
386
422
|
|
|
387
423
|
if self.restart_on_env_change:
|
|
388
|
-
# Wrap command to capture old hash before writing and output it after
|
|
389
424
|
new_env_hash = hashlib.sha256(resolved_env.encode()).hexdigest()
|
|
390
|
-
|
|
391
|
-
f"
|
|
392
|
-
|
|
393
|
-
f'echo ":::ENV_HASH:::$OLD:::ENV_HASH:::"'
|
|
425
|
+
old_hash_output, _ = conn.run(
|
|
426
|
+
f"sha256sum {remote_env_path_q} 2>/dev/null | cut -d' ' -f1 || true",
|
|
427
|
+
hide=True,
|
|
394
428
|
)
|
|
395
|
-
|
|
396
|
-
# Extract hash from between markers
|
|
397
|
-
old_env_hash = output.split(":::ENV_HASH:::")[1]
|
|
429
|
+
old_env_hash = old_hash_output.strip()
|
|
398
430
|
if old_env_hash and old_env_hash != new_env_hash:
|
|
399
431
|
self.output.info(
|
|
400
432
|
"Environment variables changed, forcing full restart..."
|
|
401
433
|
)
|
|
402
434
|
self.full_restart = True
|
|
403
|
-
else:
|
|
404
|
-
conn.run(write_env_cmd, hide=True)
|
|
405
435
|
|
|
406
|
-
|
|
436
|
+
# Upload .env via SCP (avoids shell escaping issues with base64 piping)
|
|
437
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".env") as tmp_env:
|
|
438
|
+
tmp_env.write(resolved_env)
|
|
439
|
+
tmp_env.flush()
|
|
440
|
+
conn.put(tmp_env.name, remote_env_path_q)
|
|
441
|
+
|
|
442
|
+
# chown may fail on first deploy if app_user doesn't exist yet
|
|
443
|
+
# (installer creates it), so use || true to make it non-fatal
|
|
444
|
+
conn.run(
|
|
445
|
+
f"chmod 640 {remote_env_path_q} && "
|
|
446
|
+
f"(chown {self.selected_host.user}:{self.config.app_user} {remote_env_path_q} || true)",
|
|
447
|
+
hide=True,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Run pre-install hooks (as deploy user, with .env sourced)
|
|
451
|
+
pre_install_commands = resolved_hooks.get("pre_install", [])
|
|
452
|
+
if pre_install_commands:
|
|
453
|
+
self.output.info("Running pre-install hooks...")
|
|
454
|
+
try:
|
|
455
|
+
for cmd in pre_install_commands:
|
|
456
|
+
self.output.info(f" [pre-install] {cmd}")
|
|
457
|
+
full_cmd = (
|
|
458
|
+
f"set -a; source {install_dir_q}/.env 2>/dev/null; set +a; "
|
|
459
|
+
f"{cmd}"
|
|
460
|
+
)
|
|
461
|
+
conn.run(full_cmd, pty=True)
|
|
462
|
+
except CommandError:
|
|
463
|
+
conn.run(
|
|
464
|
+
f"mv {remote_env_backup_q} {remote_env_path_q} 2>/dev/null || true",
|
|
465
|
+
hide=True,
|
|
466
|
+
)
|
|
467
|
+
raise
|
|
407
468
|
|
|
408
469
|
rollback_ran = False
|
|
409
470
|
rollback_succeeded = False
|
|
@@ -416,15 +477,24 @@ class Deploy(BaseCommand):
|
|
|
416
477
|
conn.run(install_cmd, pty=True)
|
|
417
478
|
except CommandError as e:
|
|
418
479
|
if e.code != installer.EXIT_SERVICE_START_FAILED:
|
|
480
|
+
conn.run(f"rm -f {remote_env_backup_q}", warn=True, hide=True)
|
|
419
481
|
raise DeploymentError(
|
|
420
482
|
f"Installation failed with exit code {e.code}"
|
|
421
483
|
) from e
|
|
422
484
|
|
|
423
485
|
if self.no_rollback:
|
|
486
|
+
conn.run(f"rm -f {remote_env_backup_q}", warn=True, hide=True)
|
|
424
487
|
raise DeploymentError(
|
|
425
488
|
"Services failed to start. Rollback disabled via --no-rollback."
|
|
426
489
|
) from e
|
|
427
490
|
|
|
491
|
+
# Restore previous .env before rollback
|
|
492
|
+
self.output.info("Restoring previous environment...")
|
|
493
|
+
conn.run(
|
|
494
|
+
f"mv {remote_env_backup_q} {remote_env_path_q} 2>/dev/null || true",
|
|
495
|
+
hide=True,
|
|
496
|
+
)
|
|
497
|
+
|
|
428
498
|
rollback = Rollback(host=self.host, previous=True, strict=True)
|
|
429
499
|
self.output.info(
|
|
430
500
|
"Services failed to start. Rolling back to previous version."
|
|
@@ -458,6 +528,9 @@ class Deploy(BaseCommand):
|
|
|
458
528
|
warn=True,
|
|
459
529
|
)
|
|
460
530
|
|
|
531
|
+
if not rollback_ran:
|
|
532
|
+
conn.run(f"rm -f {remote_env_backup_q}", warn=True, hide=True)
|
|
533
|
+
|
|
461
534
|
# Get git commit hash if available
|
|
462
535
|
log_operation(
|
|
463
536
|
connection=conn,
|
|
@@ -497,6 +570,34 @@ class Deploy(BaseCommand):
|
|
|
497
570
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
498
571
|
yield Path(tmpdir)
|
|
499
572
|
|
|
573
|
+
@contextmanager
|
|
574
|
+
def _deploy_session(self) -> Generator[SSH2Connection, None, None]:
|
|
575
|
+
"""Context manager that combines SSH connection + deployment lock.
|
|
576
|
+
|
|
577
|
+
Acquires the deploy lock on the remote server before yielding the
|
|
578
|
+
connection, and releases it on exit (even on error).
|
|
579
|
+
"""
|
|
580
|
+
with self.connection() as conn:
|
|
581
|
+
install_dir_q = shlex.quote(self.config.install_dir)
|
|
582
|
+
lock_file_q = shlex.quote(f"{self.config.install_dir}/.deploy_lock")
|
|
583
|
+
conn.run(f"mkdir -p {install_dir_q}", hide=True)
|
|
584
|
+
_, acquired = conn.run(
|
|
585
|
+
f"(set -C; echo $$ > {lock_file_q}) 2>/dev/null",
|
|
586
|
+
warn=True,
|
|
587
|
+
hide=True,
|
|
588
|
+
)
|
|
589
|
+
if not acquired:
|
|
590
|
+
raise DeploymentError(
|
|
591
|
+
"Another deployment is in progress.\n"
|
|
592
|
+
"If you are sure no deploy is running, remove the lock manually:\n"
|
|
593
|
+
f" ssh {self.selected_host.user}@{self.selected_host.address} rm {lock_file_q}"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
yield conn
|
|
598
|
+
finally:
|
|
599
|
+
conn.run(f"rm -f {lock_file_q}", warn=True, hide=True)
|
|
600
|
+
|
|
500
601
|
def _show_deployment_summary(self, bundle_size: int, bundle_version: str):
|
|
501
602
|
console = Console()
|
|
502
603
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from functools import cache
|
|
2
|
+
from functools import cache, cached_property
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
@@ -45,13 +45,20 @@ class SecretConfig(msgspec.Struct):
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class
|
|
48
|
+
class HooksConfig(msgspec.Struct):
|
|
49
|
+
pre_install: list[str] = msgspec.field(default_factory=list)
|
|
50
|
+
post_install: list[str] = msgspec.field(default_factory=list)
|
|
51
|
+
post_start: list[str] = msgspec.field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Config(msgspec.Struct, kw_only=True, dict=True):
|
|
49
55
|
app_name: str = msgspec.field(name="app")
|
|
50
56
|
app_user: str | None = None # User to run the app as (defaults to app_name)
|
|
51
57
|
version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
|
|
52
58
|
versions_to_keep: int | None = 5
|
|
53
59
|
python_version: str | None = None
|
|
54
60
|
build_command: str
|
|
61
|
+
build_timeout: int = 300
|
|
55
62
|
installation_mode: InstallationMode
|
|
56
63
|
distfile: str
|
|
57
64
|
aliases: dict[str, str] = msgspec.field(default_factory=dict)
|
|
@@ -70,6 +77,11 @@ class Config(msgspec.Struct, kw_only=True):
|
|
|
70
77
|
default_factory=lambda: SecretConfig(adapter="system"),
|
|
71
78
|
)
|
|
72
79
|
|
|
80
|
+
hooks_config: HooksConfig | None = msgspec.field(
|
|
81
|
+
name="hooks",
|
|
82
|
+
default=None,
|
|
83
|
+
)
|
|
84
|
+
|
|
73
85
|
def __post_init__(self):
|
|
74
86
|
if not self.app_user:
|
|
75
87
|
self.app_user = self.app_name
|
|
@@ -187,7 +199,7 @@ class Config(msgspec.Struct, kw_only=True):
|
|
|
187
199
|
def caddy_config_path(self) -> str:
|
|
188
200
|
return f"{self.caddy_config_dir}/{self.app_name}.caddy"
|
|
189
201
|
|
|
190
|
-
@
|
|
202
|
+
@cached_property
|
|
191
203
|
def deployed_units(self) -> list[DeployedUnit]:
|
|
192
204
|
return discover_deployed_units(
|
|
193
205
|
self.local_config_dir, self.app_name, self.replicas
|