odoo-devops-tools 1.25.0__tar.gz → 1.25.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.25.0
3
+ Version: 1.25.2
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -174,11 +174,11 @@ This creates `./odoo19/odoo-project.ini` with this initial content:
174
174
  version = 19.0
175
175
  ```
176
176
 
177
- Then start the Docker Compose environment:
177
+ Then prepare and start the Docker Compose environment:
178
178
 
179
179
  ```bash
180
180
  cd ./odoo19
181
- docker compose up -d --build --wait
181
+ odoo-compose up --build
182
182
  ```
183
183
 
184
184
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -194,7 +194,7 @@ Docker Compose runs both Odoo and PostgreSQL. Odoo is available at http://localh
194
194
  >
195
195
  > ```bash
196
196
  > odoo-compose setup --set docker:restart=no
197
- > docker compose up -d --build --wait
197
+ > odoo-compose up --build
198
198
  > ```
199
199
 
200
200
  ### 1.1. Adding extra addons
@@ -217,7 +217,7 @@ Sync the addon repositories and regenerate the Docker environment:
217
217
 
218
218
  ```bash
219
219
  odoo-compose setup --sync-addons
220
- docker compose up -d --build --wait
220
+ odoo-compose up --build
221
221
  ```
222
222
 
223
223
  The repositories are cloned into `ROOT/odoo-addons/` and bind-mounted into the Odoo container. Python dependencies from their `requirements.txt` files are included when rebuilding the Docker image.
@@ -256,7 +256,7 @@ Sync the Odoo source and generate the Docker artifacts:
256
256
 
257
257
  ```bash
258
258
  odoo-compose setup --sync-all
259
- docker compose up -d --build --wait
259
+ odoo-compose up --build
260
260
  ```
261
261
 
262
262
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -267,14 +267,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
267
267
 
268
268
  ```bash
269
269
  odoo-compose setup --set docker:mailpit_enabled=true
270
- docker compose up -d --build --wait
270
+ odoo-compose up
271
271
  ```
272
272
 
273
273
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
274
274
 
275
275
  ```bash
276
276
  odoo-compose setup --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
277
- docker compose up -d --build --wait
277
+ odoo-compose up
278
278
  ```
279
279
 
280
280
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -316,11 +316,10 @@ mailpit_mem_limit = 256m
316
316
 
317
317
  Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
318
318
 
319
- Regenerate the Docker configuration after editing the INI, then start the services:
319
+ Apply the updated resource limits and start the services. `up` regenerates the Compose configuration automatically, so no separate `setup` or image rebuild is necessary:
320
320
 
321
321
  ```bash
322
- odoo-compose setup
323
- docker compose up -d --build --wait
322
+ odoo-compose up
324
323
  ```
325
324
 
326
325
  ### 1.6. Creating a Docker deploy build context
@@ -388,15 +387,23 @@ db_user = odoo
388
387
  db_password = odoo
389
388
  ```
390
389
 
391
- Then start Odoo with the CLI:
390
+ Then start Odoo in the background:
392
391
 
393
392
  ```bash
394
393
  cd ./odoo19
395
- odoo-compose-venv run
394
+ odoo-compose-venv up
395
+ odoo-compose-venv status
396
396
  ```
397
397
 
398
398
  Odoo starts with the generated configuration from `./odoo-configs/odoo-server.conf` and is available at http://localhost:8069.
399
399
 
400
+ Use `odoo-compose-venv run` instead to run in the foreground:
401
+
402
+ ```bash
403
+ cd ./odoo19
404
+ odoo-compose-venv run
405
+ ```
406
+
400
407
  ### 2.1. Adding extra addons
401
408
 
402
409
  To extend Odoo with additional functionality, add extra addons through `[addons.<name>]` sections in `odoo-project.ini`.
@@ -614,6 +621,32 @@ This recreates `ROOT/venv`, skips dependency compilation and wheelhouse building
614
621
 
615
622
  ---
616
623
 
624
+ ## Runtime lifecycle
625
+
626
+ Use the same lifecycle commands with either CLI after provisioning the workspace with `setup`:
627
+
628
+ ```bash
629
+ # Docker: generate Docker config and start the Compose stack
630
+ odoo-compose up --build
631
+ odoo-compose status
632
+ odoo-compose down
633
+
634
+ # Native: generate config and start Odoo in the background
635
+ odoo-compose-venv up
636
+ odoo-compose-venv status
637
+ odoo-compose-venv down
638
+ ```
639
+
640
+ `odoo-compose up` regenerates local workspace artifacts and then runs the equivalent of `docker compose up -d --wait`.
641
+
642
+ `odoo-compose up --build` additionally builds the images (`docker compose up -d --build --wait`).
643
+
644
+ Container orchestration remains with Docker Compose, and its CLI can still be used directly for advanced or service-specific operations.
645
+
646
+ `up` requires an existing `odoo-project.ini`. It regenerates local configuration without fetching Git repositories or recreating virtualenvs.
647
+
648
+ ---
649
+
617
650
  ## Commands
618
651
 
619
652
  `odoo-compose` operates on Docker workspaces; `odoo-compose-venv` operates on native Python virtualenv workspaces. Both use `ROOT/odoo-project.ini`, but generate their own workflow artifacts. Commands operate directly through the installed CLI; generated wrapper scripts are not required.
@@ -626,6 +659,15 @@ Running either CLI without a command displays help; workspace changes are perfor
626
659
  <dt><a href="#setup"><code>setup</code></a></dt>
627
660
  <dd><p>Create or update a workspace, optionally synchronizing sources and generating artifacts.</p></dd>
628
661
 
662
+ <dt><a href="#up"><code>up</code></a></dt>
663
+ <dd><p>Regenerate workspace artifacts and start Odoo in the background.</p></dd>
664
+
665
+ <dt><a href="#down"><code>down</code></a></dt>
666
+ <dd><p>Stop the runtime without deleting data.</p></dd>
667
+
668
+ <dt><a href="#status"><code>status</code></a></dt>
669
+ <dd><p>Show the current runtime state.</p></dd>
670
+
629
671
  <dt><a href="#run"><code>run</code></a></dt>
630
672
  <dd><p>Run Odoo with additional arguments.</p></dd>
631
673
 
@@ -791,6 +833,78 @@ odoo-compose-venv setup ./odoo19 --sync-all --create-venv
791
833
  odoo-compose-venv setup ./odoo18 --set odoo:version=18.0 --sync-all --create-venv
792
834
  ```
793
835
 
836
+ ### `up`
837
+
838
+ Prepare and start the selected workspace. Requires an existing `odoo-project.ini`. Docker regenerates the local Docker build context, Odoo config and `compose.yaml`, then starts services with Docker Compose; it waits for health checks. Native regenerates its configuration and starts Odoo in the background using the existing virtualenv. Neither variant automatically synchronizes repositories or rebuilds the native virtualenv.
839
+
840
+ **Usage**
841
+
842
+ ```text
843
+ {odoo-compose|odoo-compose-venv} up [--root ROOT]
844
+ odoo-compose up [--root ROOT] [--build] [--recreate]
845
+ ```
846
+
847
+ **Options**
848
+
849
+ <dl class="cli-reference">
850
+ <dt><code>--root</code> <i>ROOT</i></dt>
851
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
852
+
853
+ <dt><code>--build</code></dt>
854
+ <dd><p>Build Docker images before starting the services (Docker only).</p></dd>
855
+
856
+ <dt><code>--recreate</code></dt>
857
+ <dd><p>Force recreation of the Odoo container, without forcing recreation of the database container (Docker only).</p></dd>
858
+ </dl>
859
+
860
+ **Notes**
861
+
862
+ Docker `up` remembers the last applied generated Odoo configuration and recreates the Odoo container when it changes, including after a separate `setup`.
863
+ Running `up` without `--build` does not guarantee that changed Python dependencies or Dockerfile instructions are built into the image.
864
+ Native `up` uses `ROOT/.odt-env/odoo-process.json` for process identity and writes runtime output to `ROOT/odoo-logs/odoo-up.log`; it restarts the tracked process if the generated config changes.
865
+
866
+ ### `down`
867
+
868
+ Stop the selected workspace, preserving data. Docker delegates to `docker compose down` **without** `--volumes`; native gracefully stops only the verified Odoo process created by `odoo-compose-venv up`.
869
+
870
+ **Usage**
871
+
872
+ ```text
873
+ {odoo-compose|odoo-compose-venv} down [--root ROOT]
874
+ ```
875
+
876
+ **Options**
877
+
878
+ <dl class="cli-reference">
879
+ <dt><code>--root</code> <i>ROOT</i></dt>
880
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
881
+ </dl>
882
+
883
+ **Notes**
884
+
885
+ Native `down` never terminates arbitrary processes found by name or port. It does not stop PostgreSQL or a separately managed systemd service. Docker `down` removes Compose-managed containers and networks, but retains named volumes.
886
+
887
+ ### `status`
888
+
889
+ Show runtime status without provisioning or changing the workspace. Docker displays the Docker Compose service table; native reports the tracked Odoo PID and whether the process is still running.
890
+
891
+ **Usage**
892
+
893
+ ```text
894
+ {odoo-compose|odoo-compose-venv} status [--root ROOT]
895
+ ```
896
+
897
+ **Options**
898
+
899
+ <dl class="cli-reference">
900
+ <dt><code>--root</code> <i>ROOT</i></dt>
901
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
902
+ </dl>
903
+
904
+ **Notes**
905
+
906
+ Native `status` verifies the recorded process identity to avoid confusing a reused PID with Odoo. It does not probe the Odoo HTTP endpoint or report application health.
907
+
794
908
  ### `run`
795
909
 
796
910
  Run Odoo directly from the selected workspace. Docker uses a one-off container; Venv uses the existing virtualenv and generated `odoo-server.conf`.
@@ -1340,6 +1454,8 @@ odoo-compose-venv update --all
1340
1454
  odoo-compose-venv neutralize
1341
1455
  ```
1342
1456
 
1457
+ The Docker example uses `docker compose stop odoo` and `docker compose up -d odoo` intentionally: only the Odoo service is stopped and restarted, while PostgreSQL remains available for the restore operations.
1458
+
1343
1459
  ### `completion`
1344
1460
 
1345
1461
  Generate shell-completion definitions for either CLI. See [Shell completion](#shell-completion) under Installation for activation instructions.
@@ -146,11 +146,11 @@ This creates `./odoo19/odoo-project.ini` with this initial content:
146
146
  version = 19.0
147
147
  ```
148
148
 
149
- Then start the Docker Compose environment:
149
+ Then prepare and start the Docker Compose environment:
150
150
 
151
151
  ```bash
152
152
  cd ./odoo19
153
- docker compose up -d --build --wait
153
+ odoo-compose up --build
154
154
  ```
155
155
 
156
156
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -166,7 +166,7 @@ Docker Compose runs both Odoo and PostgreSQL. Odoo is available at http://localh
166
166
  >
167
167
  > ```bash
168
168
  > odoo-compose setup --set docker:restart=no
169
- > docker compose up -d --build --wait
169
+ > odoo-compose up --build
170
170
  > ```
171
171
 
172
172
  ### 1.1. Adding extra addons
@@ -189,7 +189,7 @@ Sync the addon repositories and regenerate the Docker environment:
189
189
 
190
190
  ```bash
191
191
  odoo-compose setup --sync-addons
192
- docker compose up -d --build --wait
192
+ odoo-compose up --build
193
193
  ```
194
194
 
195
195
  The repositories are cloned into `ROOT/odoo-addons/` and bind-mounted into the Odoo container. Python dependencies from their `requirements.txt` files are included when rebuilding the Docker image.
@@ -228,7 +228,7 @@ Sync the Odoo source and generate the Docker artifacts:
228
228
 
229
229
  ```bash
230
230
  odoo-compose setup --sync-all
231
- docker compose up -d --build --wait
231
+ odoo-compose up --build
232
232
  ```
233
233
 
234
234
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -239,14 +239,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
239
239
 
240
240
  ```bash
241
241
  odoo-compose setup --set docker:mailpit_enabled=true
242
- docker compose up -d --build --wait
242
+ odoo-compose up
243
243
  ```
244
244
 
245
245
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
246
246
 
247
247
  ```bash
248
248
  odoo-compose setup --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
249
- docker compose up -d --build --wait
249
+ odoo-compose up
250
250
  ```
251
251
 
252
252
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -288,11 +288,10 @@ mailpit_mem_limit = 256m
288
288
 
289
289
  Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
290
290
 
291
- Regenerate the Docker configuration after editing the INI, then start the services:
291
+ Apply the updated resource limits and start the services. `up` regenerates the Compose configuration automatically, so no separate `setup` or image rebuild is necessary:
292
292
 
293
293
  ```bash
294
- odoo-compose setup
295
- docker compose up -d --build --wait
294
+ odoo-compose up
296
295
  ```
297
296
 
298
297
  ### 1.6. Creating a Docker deploy build context
@@ -360,15 +359,23 @@ db_user = odoo
360
359
  db_password = odoo
361
360
  ```
362
361
 
363
- Then start Odoo with the CLI:
362
+ Then start Odoo in the background:
364
363
 
365
364
  ```bash
366
365
  cd ./odoo19
367
- odoo-compose-venv run
366
+ odoo-compose-venv up
367
+ odoo-compose-venv status
368
368
  ```
369
369
 
370
370
  Odoo starts with the generated configuration from `./odoo-configs/odoo-server.conf` and is available at http://localhost:8069.
371
371
 
372
+ Use `odoo-compose-venv run` instead to run in the foreground:
373
+
374
+ ```bash
375
+ cd ./odoo19
376
+ odoo-compose-venv run
377
+ ```
378
+
372
379
  ### 2.1. Adding extra addons
373
380
 
374
381
  To extend Odoo with additional functionality, add extra addons through `[addons.<name>]` sections in `odoo-project.ini`.
@@ -586,6 +593,32 @@ This recreates `ROOT/venv`, skips dependency compilation and wheelhouse building
586
593
 
587
594
  ---
588
595
 
596
+ ## Runtime lifecycle
597
+
598
+ Use the same lifecycle commands with either CLI after provisioning the workspace with `setup`:
599
+
600
+ ```bash
601
+ # Docker: generate Docker config and start the Compose stack
602
+ odoo-compose up --build
603
+ odoo-compose status
604
+ odoo-compose down
605
+
606
+ # Native: generate config and start Odoo in the background
607
+ odoo-compose-venv up
608
+ odoo-compose-venv status
609
+ odoo-compose-venv down
610
+ ```
611
+
612
+ `odoo-compose up` regenerates local workspace artifacts and then runs the equivalent of `docker compose up -d --wait`.
613
+
614
+ `odoo-compose up --build` additionally builds the images (`docker compose up -d --build --wait`).
615
+
616
+ Container orchestration remains with Docker Compose, and its CLI can still be used directly for advanced or service-specific operations.
617
+
618
+ `up` requires an existing `odoo-project.ini`. It regenerates local configuration without fetching Git repositories or recreating virtualenvs.
619
+
620
+ ---
621
+
589
622
  ## Commands
590
623
 
591
624
  `odoo-compose` operates on Docker workspaces; `odoo-compose-venv` operates on native Python virtualenv workspaces. Both use `ROOT/odoo-project.ini`, but generate their own workflow artifacts. Commands operate directly through the installed CLI; generated wrapper scripts are not required.
@@ -598,6 +631,15 @@ Running either CLI without a command displays help; workspace changes are perfor
598
631
  <dt><a href="#setup"><code>setup</code></a></dt>
599
632
  <dd><p>Create or update a workspace, optionally synchronizing sources and generating artifacts.</p></dd>
600
633
 
634
+ <dt><a href="#up"><code>up</code></a></dt>
635
+ <dd><p>Regenerate workspace artifacts and start Odoo in the background.</p></dd>
636
+
637
+ <dt><a href="#down"><code>down</code></a></dt>
638
+ <dd><p>Stop the runtime without deleting data.</p></dd>
639
+
640
+ <dt><a href="#status"><code>status</code></a></dt>
641
+ <dd><p>Show the current runtime state.</p></dd>
642
+
601
643
  <dt><a href="#run"><code>run</code></a></dt>
602
644
  <dd><p>Run Odoo with additional arguments.</p></dd>
603
645
 
@@ -763,6 +805,78 @@ odoo-compose-venv setup ./odoo19 --sync-all --create-venv
763
805
  odoo-compose-venv setup ./odoo18 --set odoo:version=18.0 --sync-all --create-venv
764
806
  ```
765
807
 
808
+ ### `up`
809
+
810
+ Prepare and start the selected workspace. Requires an existing `odoo-project.ini`. Docker regenerates the local Docker build context, Odoo config and `compose.yaml`, then starts services with Docker Compose; it waits for health checks. Native regenerates its configuration and starts Odoo in the background using the existing virtualenv. Neither variant automatically synchronizes repositories or rebuilds the native virtualenv.
811
+
812
+ **Usage**
813
+
814
+ ```text
815
+ {odoo-compose|odoo-compose-venv} up [--root ROOT]
816
+ odoo-compose up [--root ROOT] [--build] [--recreate]
817
+ ```
818
+
819
+ **Options**
820
+
821
+ <dl class="cli-reference">
822
+ <dt><code>--root</code> <i>ROOT</i></dt>
823
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
824
+
825
+ <dt><code>--build</code></dt>
826
+ <dd><p>Build Docker images before starting the services (Docker only).</p></dd>
827
+
828
+ <dt><code>--recreate</code></dt>
829
+ <dd><p>Force recreation of the Odoo container, without forcing recreation of the database container (Docker only).</p></dd>
830
+ </dl>
831
+
832
+ **Notes**
833
+
834
+ Docker `up` remembers the last applied generated Odoo configuration and recreates the Odoo container when it changes, including after a separate `setup`.
835
+ Running `up` without `--build` does not guarantee that changed Python dependencies or Dockerfile instructions are built into the image.
836
+ Native `up` uses `ROOT/.odt-env/odoo-process.json` for process identity and writes runtime output to `ROOT/odoo-logs/odoo-up.log`; it restarts the tracked process if the generated config changes.
837
+
838
+ ### `down`
839
+
840
+ Stop the selected workspace, preserving data. Docker delegates to `docker compose down` **without** `--volumes`; native gracefully stops only the verified Odoo process created by `odoo-compose-venv up`.
841
+
842
+ **Usage**
843
+
844
+ ```text
845
+ {odoo-compose|odoo-compose-venv} down [--root ROOT]
846
+ ```
847
+
848
+ **Options**
849
+
850
+ <dl class="cli-reference">
851
+ <dt><code>--root</code> <i>ROOT</i></dt>
852
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
853
+ </dl>
854
+
855
+ **Notes**
856
+
857
+ Native `down` never terminates arbitrary processes found by name or port. It does not stop PostgreSQL or a separately managed systemd service. Docker `down` removes Compose-managed containers and networks, but retains named volumes.
858
+
859
+ ### `status`
860
+
861
+ Show runtime status without provisioning or changing the workspace. Docker displays the Docker Compose service table; native reports the tracked Odoo PID and whether the process is still running.
862
+
863
+ **Usage**
864
+
865
+ ```text
866
+ {odoo-compose|odoo-compose-venv} status [--root ROOT]
867
+ ```
868
+
869
+ **Options**
870
+
871
+ <dl class="cli-reference">
872
+ <dt><code>--root</code> <i>ROOT</i></dt>
873
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
874
+ </dl>
875
+
876
+ **Notes**
877
+
878
+ Native `status` verifies the recorded process identity to avoid confusing a reused PID with Odoo. It does not probe the Odoo HTTP endpoint or report application health.
879
+
766
880
  ### `run`
767
881
 
768
882
  Run Odoo directly from the selected workspace. Docker uses a one-off container; Venv uses the existing virtualenv and generated `odoo-server.conf`.
@@ -1312,6 +1426,8 @@ odoo-compose-venv update --all
1312
1426
  odoo-compose-venv neutralize
1313
1427
  ```
1314
1428
 
1429
+ The Docker example uses `docker compose stop odoo` and `docker compose up -d odoo` intentionally: only the Odoo service is stopped and restarted, while PostgreSQL remains available for the restore operations.
1430
+
1315
1431
  ### `completion`
1316
1432
 
1317
1433
  Generate shell-completion definitions for either CLI. See [Shell completion](#shell-completion) under Installation for activation instructions.
@@ -0,0 +1 @@
1
+ __version__ = "1.25.2"
@@ -9,6 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  import argparse
11
11
  import configparser
12
+ import contextlib
13
+ import ctypes
12
14
  import hashlib
13
15
  import io
14
16
  import json
@@ -18,11 +20,13 @@ import platform
18
20
  import re
19
21
  import shlex
20
22
  import shutil
23
+ import signal
21
24
  import socket
22
25
  import stat
23
26
  import subprocess
24
27
  import sys
25
28
  import tempfile
29
+ import time
26
30
  import zipfile
27
31
  from dataclasses import asdict, dataclass, field, replace
28
32
  from datetime import datetime, timezone
@@ -190,6 +194,30 @@ _INI_SET_DOCKER_SHARE_SUPPORTED_OPTIONS = {
190
194
  "target",
191
195
  }
192
196
 
197
+ _NATIVE_RUNTIME_STATE = "odoo-process.json"
198
+ _NATIVE_RUNTIME_LOCK = "odoo-process.lock"
199
+ _NATIVE_RUNTIME_LOG = "odoo-up.log"
200
+
201
+ _CLI_COMMANDS = (
202
+ ("setup", "Create or update an Odoo workspace"),
203
+ ("up", "Prepare and start the Odoo workspace"),
204
+ ("down", "Stop the Odoo workspace (keep data)"),
205
+ ("status", "Show the Odoo runtime status"),
206
+ ("completion", "Generate shell completion script (bash, zsh, fish, powershell)"),
207
+ ("run", "Run Odoo"),
208
+ ("shell", "Open an Odoo shell"),
209
+ ("update", "Update installed Odoo modules"),
210
+ ("test", "Run Odoo tests"),
211
+ ("neutralize", "Neutralize the workspace database"),
212
+ ("backup-db", "Back up the database"),
213
+ ("restore-db", "Restore the database"),
214
+ ("backup-filestore", "Back up the filestore"),
215
+ ("restore-filestore", "Restore the filestore"),
216
+ ("restic", "Run a Restic command"),
217
+ ("backup-filestore-restic", "Back up the filestore using Restic"),
218
+ ("restore-filestore-restic", "Restore a Restic filestore snapshot"),
219
+ )
220
+
193
221
 
194
222
  # -----------------------------
195
223
  # Data models
@@ -3291,7 +3319,7 @@ COPY --chown=odoo:odoo odoo/ {_DOCKER_ODOO_CONTAINER_ROOT.as_posix()}/
3291
3319
  workspace_runtime_step = f"""
3292
3320
  ENV PYTHONPATH={_DOCKER_ODOO_CONTAINER_ROOT.as_posix()}
3293
3321
 
3294
- RUN printf '%s\\n' '#!/bin/sh' 'exec {_DOCKER_ODOO_CONTAINER_ROOT.as_posix()}/odoo-bin "$@"' > /usr/local/bin/odoo \\
3322
+ RUN printf '%s\\n' '#!/bin/sh' 'exec python3 {_DOCKER_ODOO_CONTAINER_ROOT.as_posix()}/odoo-bin "$@"' > /usr/local/bin/odoo \\
3295
3323
  && chmod 0755 /usr/local/bin/odoo
3296
3324
  """
3297
3325
 
@@ -5447,26 +5475,6 @@ def _prepare_included_project_ini(
5447
5475
  return output_path
5448
5476
 
5449
5477
 
5450
-
5451
-
5452
- _CLI_COMMANDS = (
5453
- ("setup", "Create or update an Odoo workspace"),
5454
- ("completion", "Generate shell completion script (bash, zsh, fish, powershell)"),
5455
- ("run", "Run Odoo"),
5456
- ("shell", "Open an Odoo shell"),
5457
- ("update", "Update installed Odoo modules"),
5458
- ("test", "Run Odoo tests"),
5459
- ("neutralize", "Neutralize the workspace database"),
5460
- ("backup-db", "Back up the database"),
5461
- ("restore-db", "Restore the database"),
5462
- ("backup-filestore", "Back up the filestore"),
5463
- ("restore-filestore", "Restore the filestore"),
5464
- ("restic", "Run a Restic command"),
5465
- ("backup-filestore-restic", "Back up the filestore using Restic"),
5466
- ("restore-filestore-restic", "Restore a Restic filestore snapshot"),
5467
- )
5468
-
5469
-
5470
5478
  def build_parser(prog: str = "odoo-compose", target: str = "docker", *, setup_command: bool = False) -> argparse.ArgumentParser:
5471
5479
  """Build the top-level command list or the full workspace setup parser."""
5472
5480
  if not setup_command:
@@ -6383,6 +6391,317 @@ def _run_odoo_cli(target: str, action: str, args: list[str], prog: str) -> int:
6383
6391
  return 130
6384
6392
 
6385
6393
 
6394
+ # -----------------------------
6395
+ # Workspace lifecycle: Docker Compose and native virtualenv
6396
+ # -----------------------------
6397
+
6398
+ def _build_lifecycle_parser(action: str, prog: str, target: str) -> argparse.ArgumentParser:
6399
+ descriptions = {
6400
+ "up": "Regenerate the workspace configuration and start Odoo in the background.",
6401
+ "down": "Stop the running workspace without deleting databases or filestores.",
6402
+ "status": "Show the current workspace runtime status without changing anything.",
6403
+ }
6404
+ parser = argparse.ArgumentParser(prog=f"{prog} {action}", description=descriptions[action])
6405
+ parser.add_argument("--root", default=".", metavar="ROOT", help="Existing workspace root (default: current directory).")
6406
+ if action == "up" and target == "docker":
6407
+ parser.add_argument("--build", action="store_true", help="Build the Docker image before starting services.")
6408
+ parser.add_argument("--recreate", action="store_true", help="Force recreation of the Odoo container only.")
6409
+ return parser
6410
+
6411
+
6412
+ def _native_runtime_paths(root: Path) -> tuple[Path, Path, Path]:
6413
+ return (root / ".odt-env" / _NATIVE_RUNTIME_STATE,
6414
+ root / ".odt-env" / _NATIVE_RUNTIME_LOCK,
6415
+ root / "odoo-logs" / _NATIVE_RUNTIME_LOG)
6416
+
6417
+
6418
+ @contextlib.contextmanager
6419
+ def _native_runtime_lock(root: Path):
6420
+ """Serialize up/down across CLI invocations; never leave a stale lock owner."""
6421
+ _, lock_path, _ = _native_runtime_paths(root)
6422
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
6423
+ with lock_path.open("a+b") as lock_file:
6424
+ if os.name == "nt":
6425
+ import msvcrt
6426
+ lock_file.seek(0)
6427
+ lock_file.write(b"0")
6428
+ lock_file.flush()
6429
+ lock_file.seek(0)
6430
+ try:
6431
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
6432
+ except OSError as exc:
6433
+ raise _WorkspaceOperationError("another native lifecycle operation is in progress") from exc
6434
+ else:
6435
+ import fcntl
6436
+ try:
6437
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
6438
+ except OSError as exc:
6439
+ raise _WorkspaceOperationError("another native lifecycle operation is in progress") from exc
6440
+ try:
6441
+ yield
6442
+ finally:
6443
+ if os.name == "nt":
6444
+ lock_file.seek(0)
6445
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
6446
+ else:
6447
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
6448
+
6449
+
6450
+ def _native_windows_process_identity(pid: int) -> Optional[str]:
6451
+ """Read the creation time of a live Windows process to guard against PID reuse."""
6452
+ from ctypes import wintypes
6453
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
6454
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
6455
+ kernel32.OpenProcess.restype = wintypes.HANDLE
6456
+ kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
6457
+ kernel32.GetExitCodeProcess.restype = wintypes.BOOL
6458
+ kernel32.GetProcessTimes.argtypes = [wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4
6459
+ kernel32.GetProcessTimes.restype = wintypes.BOOL
6460
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
6461
+ kernel32.CloseHandle.restype = wintypes.BOOL
6462
+ handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
6463
+ if not handle:
6464
+ return None
6465
+ try:
6466
+ status = wintypes.DWORD()
6467
+ creation, exit_time, kernel_time, user_time = (wintypes.FILETIME() for _ in range(4))
6468
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(status)) or status.value != 259:
6469
+ return None # STILL_ACTIVE
6470
+ if not kernel32.GetProcessTimes(handle, ctypes.byref(creation), ctypes.byref(exit_time),
6471
+ ctypes.byref(kernel_time), ctypes.byref(user_time)):
6472
+ return None
6473
+ return f"windows:{(creation.dwHighDateTime << 32) | creation.dwLowDateTime}"
6474
+ finally:
6475
+ kernel32.CloseHandle(handle)
6476
+
6477
+
6478
+ def _native_process_identity(pid: int) -> Optional[str]:
6479
+ """Return a stable per-process creation token; refuse unverified PID reuse."""
6480
+ if not isinstance(pid, int) or pid <= 0:
6481
+ return None
6482
+ if sys.platform.startswith("linux"):
6483
+ try:
6484
+ raw = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
6485
+ fields = raw[raw.rfind(")") + 2:].split()
6486
+ if fields[0] in ("Z", "X"):
6487
+ return None
6488
+ try:
6489
+ boot_id = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip()
6490
+ except OSError:
6491
+ boot_id = "unknown"
6492
+ return f"linux:{boot_id}:{fields[19]}" # field 22: starttime in clock ticks
6493
+ except (OSError, IndexError, ValueError):
6494
+ return None
6495
+ if os.name == "nt":
6496
+ return _native_windows_process_identity(pid)
6497
+ # macOS and other POSIX systems: start timestamp from ps.
6498
+ result = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="],
6499
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False)
6500
+ created = result.stdout.strip()
6501
+ return f"posix:{created}" if result.returncode == 0 and created else None
6502
+
6503
+
6504
+ def _native_read_process(root: Path) -> Optional[dict[str, Any]]:
6505
+ state_path, _, _ = _native_runtime_paths(root)
6506
+ try:
6507
+ state = json.loads(state_path.read_text(encoding="utf-8"))
6508
+ except FileNotFoundError:
6509
+ return None
6510
+ except (ValueError, OSError) as exc:
6511
+ raise _WorkspaceOperationError(f"cannot read native process state: {state_path}") from exc
6512
+ if not isinstance(state, dict) or not isinstance(state.get("pid"), int) or not isinstance(state.get("identity"), str):
6513
+ raise _WorkspaceOperationError(f"invalid native process state: {state_path}")
6514
+ # Compare filesystem identity instead of path spelling: Windows 8.3 aliases
6515
+ # and symlinks may represent the same workspace under different names.
6516
+ saved_root = state.get("root")
6517
+ try:
6518
+ same_workspace = isinstance(saved_root, str) and Path(saved_root).samefile(root)
6519
+ except (OSError, ValueError):
6520
+ same_workspace = False
6521
+ if not same_workspace:
6522
+ raise _WorkspaceOperationError("native process state belongs to a different workspace")
6523
+ return state
6524
+
6525
+
6526
+ def _native_process_running(state: dict[str, Any]) -> bool:
6527
+ return _native_process_identity(state["pid"]) == state["identity"]
6528
+
6529
+
6530
+ def _native_wait_stopped(state: dict[str, Any], timeout: float) -> bool:
6531
+ deadline = time.monotonic() + timeout
6532
+ while time.monotonic() < deadline:
6533
+ if not _native_process_running(state):
6534
+ return True
6535
+ time.sleep(0.2)
6536
+ return not _native_process_running(state)
6537
+
6538
+
6539
+ def _native_stop_process(state: dict[str, Any]) -> None:
6540
+ """Only terminate our verified Odoo process/process group, never arbitrary PID matches."""
6541
+ if not _native_process_running(state):
6542
+ return
6543
+ pid = state["pid"]
6544
+ if os.name == "nt":
6545
+ # A new process group permits a graceful CTRL_BREAK when a console exists.
6546
+ try:
6547
+ os.kill(pid, signal.CTRL_BREAK_EVENT)
6548
+ except OSError:
6549
+ # A non-interactive Windows session may have no console to signal.
6550
+ subprocess.run(["taskkill", "/PID", str(pid), "/T"],
6551
+ check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
6552
+ else:
6553
+ # up starts a dedicated session; refuse to signal any unrelated group.
6554
+ if os.getpgid(pid) != pid:
6555
+ raise _WorkspaceOperationError("Odoo is no longer the leader of its recorded process group")
6556
+ os.killpg(pid, signal.SIGTERM)
6557
+ if _native_wait_stopped(state, 10):
6558
+ return
6559
+ if not _native_process_running(state):
6560
+ return
6561
+ if os.name == "nt":
6562
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
6563
+ check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
6564
+ else:
6565
+ if os.getpgid(pid) != pid:
6566
+ raise _WorkspaceOperationError("Odoo process group changed while stopping")
6567
+ os.killpg(pid, signal.SIGKILL)
6568
+ if not _native_wait_stopped(state, 5):
6569
+ raise _WorkspaceOperationError(f"Odoo process {pid} did not stop")
6570
+
6571
+
6572
+ def _native_start_process(root: Path, cfg: ProjectConfig, conf: Path) -> dict[str, Any]:
6573
+ layout = Layout.from_root(root)
6574
+ python = root / "venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
6575
+ odoo_bin = _resolve_odoo_path(layout, cfg.odoo) / "odoo-bin"
6576
+ if not python.is_file() or not odoo_bin.is_file():
6577
+ raise _WorkspaceOperationError("native Odoo Python or odoo-bin not found; run setup first")
6578
+ if not conf.is_file():
6579
+ raise _WorkspaceOperationError("generated native Odoo config is missing")
6580
+ state_path, _, log_path = _native_runtime_paths(root)
6581
+ log_path.parent.mkdir(parents=True, exist_ok=True)
6582
+ command = [str(python), str(odoo_bin), "-c", str(conf)]
6583
+ flags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
6584
+ with log_path.open("ab") as log_file:
6585
+ proc = subprocess.Popen(command, cwd=root, stdin=subprocess.DEVNULL,
6586
+ stdout=log_file, stderr=subprocess.STDOUT, close_fds=True,
6587
+ creationflags=flags, start_new_session=(os.name != "nt"))
6588
+ # A process may exit immediately (bad interpreter, invalid config, etc.).
6589
+ time.sleep(0.3)
6590
+ identity = _native_process_identity(proc.pid)
6591
+ if proc.poll() is not None or identity is None:
6592
+ raise _WorkspaceOperationError(f"Odoo exited during startup; check {log_path}")
6593
+ state = {"pid": proc.pid, "identity": identity, "root": str(root),
6594
+ "config_sha256": _sha256_file(conf)}
6595
+ try:
6596
+ _write_json_atomic(state_path, state)
6597
+ except Exception:
6598
+ # Never leave a detached process unmanaged if recording it fails.
6599
+ _native_stop_process(state)
6600
+ raise
6601
+ print(f"Odoo started (PID {proc.pid}); logs: {log_path}")
6602
+ return state
6603
+
6604
+
6605
+ def _run_lifecycle_cli(target: str, action: str, args: list[str], prog: str) -> int:
6606
+ parser = _build_lifecycle_parser(action, prog, target)
6607
+ opts = parser.parse_args(args)
6608
+ root = Path(opts.root).expanduser().resolve()
6609
+ if not root.is_dir():
6610
+ parser.error(f"workspace ROOT does not exist: {root}")
6611
+ ini = root / _DEFAULT_PROJECT_INI_NAME
6612
+ if not ini.is_file():
6613
+ parser.error(f"workspace project file not found: {ini}; run setup first")
6614
+ try:
6615
+ if target == "docker":
6616
+ compose = _workspace_docker_command() + ["compose"]
6617
+ if action == "up":
6618
+ conf = _docker_odoo_conf_path(Layout.from_root(root).docker_local_dir)
6619
+ runtime_path = root / ".odt-env" / "docker-odoo-runtime.json"
6620
+ try:
6621
+ previous = json.loads(runtime_path.read_text(encoding="utf-8"))
6622
+ if not isinstance(previous, dict):
6623
+ previous = {}
6624
+ except (OSError, ValueError):
6625
+ previous = {}
6626
+ # Provision existing sources only; never clone repos, install packages or create a new project.
6627
+ sync_project(ini, sync_odoo=False, sync_addons=False, root_override=root,
6628
+ target="docker", no_provisioning_log=True)
6629
+ new_hash = _sha256_file(conf)
6630
+ preexisting = subprocess.run(compose + ["ps", "-q", "odoo"], cwd=root,
6631
+ text=True, capture_output=True, check=False)
6632
+ if preexisting.returncode:
6633
+ raise _WorkspaceOperationError("could not query Docker Compose Odoo container")
6634
+ up = compose + ["up", "-d", "--wait"]
6635
+ if opts.build:
6636
+ up.append("--build")
6637
+ _workspace_command(up, root)
6638
+ current = subprocess.run(compose + ["ps", "-q", "odoo"], cwd=root,
6639
+ text=True, capture_output=True, check=False)
6640
+ if current.returncode or not current.stdout.strip():
6641
+ raise _WorkspaceOperationError("could not find running Docker Compose Odoo container")
6642
+ container_id = current.stdout.strip()
6643
+ if (preexisting.stdout.strip() == container_id and
6644
+ (opts.recreate or previous.get("config_sha256") != new_hash or
6645
+ previous.get("container_id") != container_id)):
6646
+ # A bind-mounted odoo.conf change is invisible to Compose config hashing.
6647
+ # Recreate only Odoo; do not restart PostgreSQL or delete its data.
6648
+ _workspace_command(compose + ["up", "-d", "--wait", "--no-deps",
6649
+ "--force-recreate", "odoo"], root)
6650
+ current = subprocess.run(compose + ["ps", "-q", "odoo"], cwd=root,
6651
+ text=True, capture_output=True, check=False)
6652
+ if current.returncode or not current.stdout.strip():
6653
+ raise _WorkspaceOperationError("could not query recreated Odoo container")
6654
+ container_id = current.stdout.strip()
6655
+ _write_json_atomic(runtime_path, {"container_id": container_id, "config_sha256": new_hash})
6656
+ return 0
6657
+ if action == "down":
6658
+ _workspace_command(compose + ["down"], root)
6659
+ (root / ".odt-env" / "docker-odoo-runtime.json").unlink(missing_ok=True)
6660
+ return 0
6661
+ _workspace_command(compose + ["ps", "-a"], root)
6662
+ return 0
6663
+
6664
+ state_path, _, log_path = _native_runtime_paths(root)
6665
+ if action == "status":
6666
+ state = _native_read_process(root)
6667
+ if state and _native_process_running(state):
6668
+ print(f"Odoo: running (PID {state['pid']})\nLogs: {log_path}")
6669
+ else:
6670
+ print("Odoo: stopped" + (" (stale process state)" if state else ""))
6671
+ return 0
6672
+ with _native_runtime_lock(root):
6673
+ state = _native_read_process(root)
6674
+ running = bool(state and _native_process_running(state))
6675
+ if action == "down":
6676
+ if running:
6677
+ _native_stop_process(state)
6678
+ print("Odoo: stopped")
6679
+ else:
6680
+ print("Odoo: already stopped")
6681
+ state_path.unlink(missing_ok=True)
6682
+ return 0
6683
+ conf = Layout.from_root(root).conf_path
6684
+ # Native up requires a provisioned environment. Never recreate the venv.
6685
+ if not (root / "venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).is_file():
6686
+ raise _WorkspaceOperationError("native virtualenv not found; run setup --create-venv first")
6687
+ sync_project(ini, sync_odoo=False, sync_addons=False, root_override=root,
6688
+ target="venv", no_provisioning_log=True)
6689
+ if running and state.get("config_sha256") == _sha256_file(conf):
6690
+ print(f"Odoo: already running (PID {state['pid']})")
6691
+ return 0
6692
+ cfg = load_project_config(ini, log_resolved=False, log_overrides=False)
6693
+ if running:
6694
+ _native_stop_process(state)
6695
+ state_path.unlink(missing_ok=True)
6696
+ elif state:
6697
+ state_path.unlink(missing_ok=True) # never signal a reused PID
6698
+ _native_start_process(root, cfg, conf)
6699
+ return 0
6700
+ except (OSError, ValueError, configparser.Error, _WorkspaceOperationError) as exc:
6701
+ _logger.error("Could not %s workspace: %s", action, exc)
6702
+ return 1
6703
+
6704
+
6386
6705
  def _completion_options(parser: argparse.ArgumentParser) -> list[str]:
6387
6706
  return list(dict.fromkeys(option for action in parser._actions for option in action.option_strings))
6388
6707
 
@@ -6642,6 +6961,7 @@ def _run_completion_cli(target: str, args: list[str], prog: str) -> int:
6642
6961
  parsers = {
6643
6962
  "": build_parser(prog, target),
6644
6963
  "setup": build_parser(f"{prog} setup", target, setup_command=True),
6964
+ **{name: _build_lifecycle_parser(name, prog, target) for name in ("up", "down", "status")},
6645
6965
  "completion": parser,
6646
6966
  **{name: _build_odoo_parser(name, prog) for name in ("run", "shell", "update", "test")},
6647
6967
  "neutralize": _build_neutralize_parser(prog),
@@ -6658,6 +6978,8 @@ def main(target: str = "docker", prog: str = "odoo-compose") -> None:
6658
6978
  stream=sys.stdout,
6659
6979
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
6660
6980
  )
6981
+ if sys.argv[1:2] and sys.argv[1] in ("up", "down", "status"):
6982
+ raise SystemExit(_run_lifecycle_cli(target, sys.argv[1], sys.argv[2:], prog))
6661
6983
  if sys.argv[1:2] == ["completion"]:
6662
6984
  raise SystemExit(_run_completion_cli(target, sys.argv[2:], prog))
6663
6985
  if sys.argv[1:2] and sys.argv[1] in ("run", "shell", "update", "test"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.25.0
3
+ Version: 1.25.2
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -174,11 +174,11 @@ This creates `./odoo19/odoo-project.ini` with this initial content:
174
174
  version = 19.0
175
175
  ```
176
176
 
177
- Then start the Docker Compose environment:
177
+ Then prepare and start the Docker Compose environment:
178
178
 
179
179
  ```bash
180
180
  cd ./odoo19
181
- docker compose up -d --build --wait
181
+ odoo-compose up --build
182
182
  ```
183
183
 
184
184
  Optionally initialize the Odoo database from the command line instead of through the web interface:
@@ -194,7 +194,7 @@ Docker Compose runs both Odoo and PostgreSQL. Odoo is available at http://localh
194
194
  >
195
195
  > ```bash
196
196
  > odoo-compose setup --set docker:restart=no
197
- > docker compose up -d --build --wait
197
+ > odoo-compose up --build
198
198
  > ```
199
199
 
200
200
  ### 1.1. Adding extra addons
@@ -217,7 +217,7 @@ Sync the addon repositories and regenerate the Docker environment:
217
217
 
218
218
  ```bash
219
219
  odoo-compose setup --sync-addons
220
- docker compose up -d --build --wait
220
+ odoo-compose up --build
221
221
  ```
222
222
 
223
223
  The repositories are cloned into `ROOT/odoo-addons/` and bind-mounted into the Odoo container. Python dependencies from their `requirements.txt` files are included when rebuilding the Docker image.
@@ -256,7 +256,7 @@ Sync the Odoo source and generate the Docker artifacts:
256
256
 
257
257
  ```bash
258
258
  odoo-compose setup --sync-all
259
- docker compose up -d --build --wait
259
+ odoo-compose up --build
260
260
  ```
261
261
 
262
262
  The resolved Odoo source is bind-mounted read-only at `/opt/odoo`. The Odoo Python source from the base image is removed while its installed dependencies remain available.
@@ -267,14 +267,14 @@ Enable Mailpit in the local Docker workflow when you need to inspect or test ema
267
267
 
268
268
  ```bash
269
269
  odoo-compose setup --set docker:mailpit_enabled=true
270
- docker compose up -d --build --wait
270
+ odoo-compose up
271
271
  ```
272
272
 
273
273
  Mailpit is available at http://localhost:8025 by default. When running multiple workspaces, override the host web UI port, for example:
274
274
 
275
275
  ```bash
276
276
  odoo-compose setup --set docker:mailpit_enabled=true --set docker:mailpit_webui_port=8026
277
- docker compose up -d --build --wait
277
+ odoo-compose up
278
278
  ```
279
279
 
280
280
  The generated local Odoo configuration sends outgoing email to `mailpit:1025`. When Mailpit is enabled, local Docker SMTP connection settings (`smtp_server`, `smtp_port`, `smtp_ssl`, `smtp_user`, and `smtp_password`) are intentionally overridden; the deploy configuration keeps the values from `[config]` unchanged.
@@ -316,11 +316,10 @@ mailpit_mem_limit = 256m
316
316
 
317
317
  Each option is independent and optional. If an option is missing or empty, `odoo-compose` omits that limit from the Compose service; no CPU or memory limit is imposed by the generator. CPU values must be positive decimal numbers (such as `0.5` or `2.0`). Memory limits must be positive amounts (such as `256m`, `2g`, or `4g`); zero and negative values are rejected. A memory limit is a hard container limit: exceeding it may cause an out-of-memory termination, so size it for your workload.
318
318
 
319
- Regenerate the Docker configuration after editing the INI, then start the services:
319
+ Apply the updated resource limits and start the services. `up` regenerates the Compose configuration automatically, so no separate `setup` or image rebuild is necessary:
320
320
 
321
321
  ```bash
322
- odoo-compose setup
323
- docker compose up -d --build --wait
322
+ odoo-compose up
324
323
  ```
325
324
 
326
325
  ### 1.6. Creating a Docker deploy build context
@@ -388,15 +387,23 @@ db_user = odoo
388
387
  db_password = odoo
389
388
  ```
390
389
 
391
- Then start Odoo with the CLI:
390
+ Then start Odoo in the background:
392
391
 
393
392
  ```bash
394
393
  cd ./odoo19
395
- odoo-compose-venv run
394
+ odoo-compose-venv up
395
+ odoo-compose-venv status
396
396
  ```
397
397
 
398
398
  Odoo starts with the generated configuration from `./odoo-configs/odoo-server.conf` and is available at http://localhost:8069.
399
399
 
400
+ Use `odoo-compose-venv run` instead to run in the foreground:
401
+
402
+ ```bash
403
+ cd ./odoo19
404
+ odoo-compose-venv run
405
+ ```
406
+
400
407
  ### 2.1. Adding extra addons
401
408
 
402
409
  To extend Odoo with additional functionality, add extra addons through `[addons.<name>]` sections in `odoo-project.ini`.
@@ -614,6 +621,32 @@ This recreates `ROOT/venv`, skips dependency compilation and wheelhouse building
614
621
 
615
622
  ---
616
623
 
624
+ ## Runtime lifecycle
625
+
626
+ Use the same lifecycle commands with either CLI after provisioning the workspace with `setup`:
627
+
628
+ ```bash
629
+ # Docker: generate Docker config and start the Compose stack
630
+ odoo-compose up --build
631
+ odoo-compose status
632
+ odoo-compose down
633
+
634
+ # Native: generate config and start Odoo in the background
635
+ odoo-compose-venv up
636
+ odoo-compose-venv status
637
+ odoo-compose-venv down
638
+ ```
639
+
640
+ `odoo-compose up` regenerates local workspace artifacts and then runs the equivalent of `docker compose up -d --wait`.
641
+
642
+ `odoo-compose up --build` additionally builds the images (`docker compose up -d --build --wait`).
643
+
644
+ Container orchestration remains with Docker Compose, and its CLI can still be used directly for advanced or service-specific operations.
645
+
646
+ `up` requires an existing `odoo-project.ini`. It regenerates local configuration without fetching Git repositories or recreating virtualenvs.
647
+
648
+ ---
649
+
617
650
  ## Commands
618
651
 
619
652
  `odoo-compose` operates on Docker workspaces; `odoo-compose-venv` operates on native Python virtualenv workspaces. Both use `ROOT/odoo-project.ini`, but generate their own workflow artifacts. Commands operate directly through the installed CLI; generated wrapper scripts are not required.
@@ -626,6 +659,15 @@ Running either CLI without a command displays help; workspace changes are perfor
626
659
  <dt><a href="#setup"><code>setup</code></a></dt>
627
660
  <dd><p>Create or update a workspace, optionally synchronizing sources and generating artifacts.</p></dd>
628
661
 
662
+ <dt><a href="#up"><code>up</code></a></dt>
663
+ <dd><p>Regenerate workspace artifacts and start Odoo in the background.</p></dd>
664
+
665
+ <dt><a href="#down"><code>down</code></a></dt>
666
+ <dd><p>Stop the runtime without deleting data.</p></dd>
667
+
668
+ <dt><a href="#status"><code>status</code></a></dt>
669
+ <dd><p>Show the current runtime state.</p></dd>
670
+
629
671
  <dt><a href="#run"><code>run</code></a></dt>
630
672
  <dd><p>Run Odoo with additional arguments.</p></dd>
631
673
 
@@ -791,6 +833,78 @@ odoo-compose-venv setup ./odoo19 --sync-all --create-venv
791
833
  odoo-compose-venv setup ./odoo18 --set odoo:version=18.0 --sync-all --create-venv
792
834
  ```
793
835
 
836
+ ### `up`
837
+
838
+ Prepare and start the selected workspace. Requires an existing `odoo-project.ini`. Docker regenerates the local Docker build context, Odoo config and `compose.yaml`, then starts services with Docker Compose; it waits for health checks. Native regenerates its configuration and starts Odoo in the background using the existing virtualenv. Neither variant automatically synchronizes repositories or rebuilds the native virtualenv.
839
+
840
+ **Usage**
841
+
842
+ ```text
843
+ {odoo-compose|odoo-compose-venv} up [--root ROOT]
844
+ odoo-compose up [--root ROOT] [--build] [--recreate]
845
+ ```
846
+
847
+ **Options**
848
+
849
+ <dl class="cli-reference">
850
+ <dt><code>--root</code> <i>ROOT</i></dt>
851
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
852
+
853
+ <dt><code>--build</code></dt>
854
+ <dd><p>Build Docker images before starting the services (Docker only).</p></dd>
855
+
856
+ <dt><code>--recreate</code></dt>
857
+ <dd><p>Force recreation of the Odoo container, without forcing recreation of the database container (Docker only).</p></dd>
858
+ </dl>
859
+
860
+ **Notes**
861
+
862
+ Docker `up` remembers the last applied generated Odoo configuration and recreates the Odoo container when it changes, including after a separate `setup`.
863
+ Running `up` without `--build` does not guarantee that changed Python dependencies or Dockerfile instructions are built into the image.
864
+ Native `up` uses `ROOT/.odt-env/odoo-process.json` for process identity and writes runtime output to `ROOT/odoo-logs/odoo-up.log`; it restarts the tracked process if the generated config changes.
865
+
866
+ ### `down`
867
+
868
+ Stop the selected workspace, preserving data. Docker delegates to `docker compose down` **without** `--volumes`; native gracefully stops only the verified Odoo process created by `odoo-compose-venv up`.
869
+
870
+ **Usage**
871
+
872
+ ```text
873
+ {odoo-compose|odoo-compose-venv} down [--root ROOT]
874
+ ```
875
+
876
+ **Options**
877
+
878
+ <dl class="cli-reference">
879
+ <dt><code>--root</code> <i>ROOT</i></dt>
880
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
881
+ </dl>
882
+
883
+ **Notes**
884
+
885
+ Native `down` never terminates arbitrary processes found by name or port. It does not stop PostgreSQL or a separately managed systemd service. Docker `down` removes Compose-managed containers and networks, but retains named volumes.
886
+
887
+ ### `status`
888
+
889
+ Show runtime status without provisioning or changing the workspace. Docker displays the Docker Compose service table; native reports the tracked Odoo PID and whether the process is still running.
890
+
891
+ **Usage**
892
+
893
+ ```text
894
+ {odoo-compose|odoo-compose-venv} status [--root ROOT]
895
+ ```
896
+
897
+ **Options**
898
+
899
+ <dl class="cli-reference">
900
+ <dt><code>--root</code> <i>ROOT</i></dt>
901
+ <dd><p>Select an existing workspace (default: current directory).</p></dd>
902
+ </dl>
903
+
904
+ **Notes**
905
+
906
+ Native `status` verifies the recorded process identity to avoid confusing a reused PID with Odoo. It does not probe the Odoo HTTP endpoint or report application health.
907
+
794
908
  ### `run`
795
909
 
796
910
  Run Odoo directly from the selected workspace. Docker uses a one-off container; Venv uses the existing virtualenv and generated `odoo-server.conf`.
@@ -1340,6 +1454,8 @@ odoo-compose-venv update --all
1340
1454
  odoo-compose-venv neutralize
1341
1455
  ```
1342
1456
 
1457
+ The Docker example uses `docker compose stop odoo` and `docker compose up -d odoo` intentionally: only the Odoo service is stopped and restarted, while PostgreSQL remains available for the restore operations.
1458
+
1343
1459
  ### `completion`
1344
1460
 
1345
1461
  Generate shell-completion definitions for either CLI. See [Shell completion](#shell-completion) under Installation for activation instructions.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odoo-devops-tools"
7
- version = "1.25.0"
7
+ version = "1.25.2"
8
8
  description = "Build reproducible Odoo workspaces from a single project definition."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "1.25.0"