gOdoo-cli 1.0.4__tar.gz → 1.0.6__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 (185) hide show
  1. godoo_cli-1.0.6/.agents/skills/godoo-release-management/SKILL.md +24 -0
  2. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.agents/skills/godoo-source-workspaces/SKILL.md +1 -1
  3. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/AGENTS.md +2 -9
  4. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/PKG-INFO +1 -1
  5. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docs/hooks.md +56 -23
  6. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docs/workspace.md +12 -11
  7. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/__about__.py +1 -1
  8. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/init.py +45 -4
  9. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/git/repository.py +0 -16
  10. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/checksums.py +19 -7
  11. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/hooks.py +46 -7
  12. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/git.py +69 -33
  13. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_checksums.py +11 -0
  14. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_hooks.py +45 -3
  15. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_init_hooks.py +149 -0
  16. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace_git.py +109 -21
  17. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.agents/skills/godoo-changeset-review/SKILL.md +0 -0
  18. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.agents/skills/godoo-editor-validation/SKILL.md +0 -0
  19. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.agents/skills/godoo-runtime-validation/SKILL.md +0 -0
  20. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.codecovrc +0 -0
  21. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.dockerignore +0 -0
  22. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.env.sample +0 -0
  23. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.github/FUNDING.yml +0 -0
  24. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.github/workflows/README.md +0 -0
  25. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.github/workflows/quality.yml +0 -0
  26. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.github/workflows/version-bump.yml +0 -0
  27. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.github/workflows/version-publish.yml +0 -0
  28. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.gitignore +0 -0
  29. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.pre-commit-config.yaml +0 -0
  30. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.prettierrc.yml +0 -0
  31. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.vscode/extensions.json +0 -0
  32. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.vscode/settings.json +0 -0
  33. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/.vscode/tasks.json +0 -0
  34. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/LICENSE +0 -0
  35. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/README.md +0 -0
  36. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/.gitkeep +0 -0
  37. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/__init__.py +0 -0
  38. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/__manifest__.py +0 -0
  39. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/models/__init__.py +0 -0
  40. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/tests/__init__.py +0 -0
  41. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/tests/data.py +0 -0
  42. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/addons/godoo_test_succ_mod/tests/test_succ.py +0 -0
  43. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/assets/odoo_logo.png +0 -0
  44. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/config/nginx.conf.template +0 -0
  45. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/config/postgresql.conf +0 -0
  46. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docker/Dockerfile +0 -0
  47. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docker/docker-compose.base.yml +0 -0
  48. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docker/docker-compose.dev.yml +0 -0
  49. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docker/docker-compose.traefik.yml +0 -0
  50. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docker/postgres-init/10-vector.sql +0 -0
  51. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docs/architecture.md +0 -0
  52. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docs/downstream.md +0 -0
  53. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/docs/lifecycle.md +0 -0
  54. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/makefile +0 -0
  55. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/odoo_manifest.yml +0 -0
  56. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/package-lock.json +0 -0
  57. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/package.json +0 -0
  58. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/pyproject.toml +0 -0
  59. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/scripts/check_docstring_style.py +0 -0
  60. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/scripts/migrations/development/shrink_database.py +0 -0
  61. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/__init__.py +0 -0
  62. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/__main__.py +0 -0
  63. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/__init__.py +0 -0
  64. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/common.py +0 -0
  65. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/configuration.py +0 -0
  66. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/__init__.py +0 -0
  67. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/archive.py +0 -0
  68. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/cli.py +0 -0
  69. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/passwords.py +0 -0
  70. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/query.py +0 -0
  71. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/db/reset.py +0 -0
  72. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/odoo_bin/__init__.py +0 -0
  73. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/root.py +0 -0
  74. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/__init__.py +0 -0
  75. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/cli.py +0 -0
  76. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/config_parameters.py +0 -0
  77. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/importer.py +0 -0
  78. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/modules.py +0 -0
  79. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/rpc/translations.py +0 -0
  80. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/run.py +0 -0
  81. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/__init__.py +0 -0
  82. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/dependencies.py +0 -0
  83. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/launch.py +0 -0
  84. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/shell.py +0 -0
  85. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/shell_scripts/odoo_banner.py +0 -0
  86. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/shell_scripts/render_report.py +0 -0
  87. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/start.py +0 -0
  88. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/runtime/status.py +0 -0
  89. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/test/__init__.py +0 -0
  90. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/test/cli.py +0 -0
  91. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/test/load_data.py +0 -0
  92. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/test/run.py +0 -0
  93. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/upgrade.py +0 -0
  94. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/__init__.py +0 -0
  95. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/common.py +0 -0
  96. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/configure.py +0 -0
  97. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/image.py +0 -0
  98. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/storage.py +0 -0
  99. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/commands/workspace/sync.py +0 -0
  100. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/database/__init__.py +0 -0
  101. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/database/connection.py +0 -0
  102. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/database/postgres.py +0 -0
  103. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/database/settings.py +0 -0
  104. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/database/state.py +0 -0
  105. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/git/__init__.py +0 -0
  106. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/git/git_url.py +0 -0
  107. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/helpers/__init__.py +0 -0
  108. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/helpers/hashing.py +0 -0
  109. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/helpers/modules_git.py +0 -0
  110. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/helpers/progress.py +0 -0
  111. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/helpers/system.py +0 -0
  112. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/models/__init__.py +0 -0
  113. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/models/godoo_git_repo.py +0 -0
  114. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/models/godoo_manifest.py +0 -0
  115. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/models/godoo_models.py +0 -0
  116. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/models/godoo_modules.py +0 -0
  117. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/__init__.py +0 -0
  118. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/archive.py +0 -0
  119. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/backup.py +0 -0
  120. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/baseline.py +0 -0
  121. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/dataset.py +0 -0
  122. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/filestore.py +0 -0
  123. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/lifecycle.py +0 -0
  124. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/locks.py +0 -0
  125. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/nginx.py +0 -0
  126. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/odoo.py +0 -0
  127. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/promotion.py +0 -0
  128. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/reset.py +0 -0
  129. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/restore.py +0 -0
  130. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/runtime/status.py +0 -0
  131. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/__init__.py +0 -0
  132. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/editor.py +0 -0
  133. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/image.py +0 -0
  134. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/manifest.py +0 -0
  135. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/operations.py +0 -0
  136. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/specifications.py +0 -0
  137. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/storage.py +0 -0
  138. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/src/godoo_cli/workspace/types.py +0 -0
  139. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/__init__.py +0 -0
  140. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_db_archive_cli.py +0 -0
  141. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_db_query_cli.py +0 -0
  142. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_odoo_bin_commands.py +0 -0
  143. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_rpc_commands.py +0 -0
  144. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_runtime_cli.py +0 -0
  145. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_runtime_dependencies.py +0 -0
  146. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_runtime_shell_scripts.py +0 -0
  147. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_runtime_start.py +0 -0
  148. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_test_run.py +0 -0
  149. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/commands/test_upgrade.py +0 -0
  150. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/conftest.py +0 -0
  151. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/database/test_db_backup.py +0 -0
  152. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/database/test_db_query.py +0 -0
  153. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/database/test_db_reset.py +0 -0
  154. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/database/test_db_restore.py +0 -0
  155. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/git/test_git_url_uri.py +0 -0
  156. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/integration/test_container_contract.py +0 -0
  157. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/integration/test_odoo_integration.py +0 -0
  158. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_archive_crc.py +0 -0
  159. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_baseline_recovery.py +0 -0
  160. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_database_copy.py +0 -0
  161. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_dataset.py +0 -0
  162. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_filestore.py +0 -0
  163. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_filestore_copy.py +0 -0
  164. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_init_policy.py +0 -0
  165. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_lifecycle.py +0 -0
  166. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_lifecycle_service.py +0 -0
  167. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_nginx.py +0 -0
  168. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_nginx_cli.py +0 -0
  169. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_odoo_config_generation.py +0 -0
  170. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_odoo_preflight.py +0 -0
  171. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_odoo_version_compatibility.py +0 -0
  172. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_report_url.py +0 -0
  173. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_reset_baseline.py +0 -0
  174. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_runtime_models.py +0 -0
  175. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_runtime_operations.py +0 -0
  176. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/runtime/test_runtime_source_environment.py +0 -0
  177. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/test_logging.py +0 -0
  178. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_godoo_manifest.py +0 -0
  179. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace.py +0 -0
  180. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace_image.py +0 -0
  181. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace_layout.py +0 -0
  182. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace_runtime_env.py +0 -0
  183. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/tests/workspace/test_workspace_storage.py +0 -0
  184. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/thirdparty/.gitkeep +0 -0
  185. {godoo_cli-1.0.4 → godoo_cli-1.0.6}/uv.lock +0 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: godoo-release-management
3
+ description:
4
+ Prepare, review, merge, and verify gOdoo releases using the repository's version-bump and publication workflows.
5
+ ---
6
+
7
+ # gOdoo release management
8
+
9
+ Read `.github/workflows/README.md` and inspect the current release state before acting. Publish or merge only when
10
+ explicitly authorized. Keep the release PR short-lived and coordinate other merges to preserve its intended scope.
11
+
12
+ Dispatch `.github/workflows/version-bump.yml` against `main` with the selected bump type. The workflow creates
13
+ `release/<version>` and its release PR; leave version changes to it and keep the PR limited to release preparation. Do
14
+ not use long-lived `release` or `develop` branches for this workflow.
15
+
16
+ The release PR description becomes the release notes. Never replace existing release text or remove its changelog
17
+ summary. If additions are needed, replace the placeholder and preserve the generated changelog and remaining text.
18
+ Review the notes, release-preparation diff, release label, source repository, target branch, title, branch name, package
19
+ version, and available Quality results against the identities in `.github/workflows/README.md` before merging.
20
+
21
+ After the authorized merge, verify publication to PyPI, the `v<version>` tag at the release merge commit, and the GitHub
22
+ Release. If publication fails, inspect the failed step and existing PyPI, tag, and release state before retrying. Never
23
+ move an existing release tag to repair publication. Report the released version, revision, validation, and any
24
+ downstream adoption required.
@@ -12,7 +12,7 @@ validated before locks or paths and cannot alter host/container depth. Git owns
12
12
  archive caches below the shared source root. Never hand-edit managed worktrees or generated `.godoo/` and workspace
13
13
  files.
14
14
 
15
- Run `workspace sync` only with authority for the shared source root; it may reset verified managed worktrees.
15
+ Run `workspace sync` only with authority for the shared source root; it updates verified managed worktrees.
16
16
  `workspace configure` regenerates state from checked sources. `workspace check` is offline and read-only. Run workspace
17
17
  commands from the project directory because that cwd supplies `.env` and manifest-relative paths.
18
18
 
@@ -10,8 +10,8 @@ coherent change.
10
10
  detects or orchestrates containers.
11
11
  - Load the matching skill under `.agents/skills/` for source-workspace or runtime work; load both only when a change
12
12
  crosses those boundaries.
13
- - Use `godoo-editor-validation` for VS Code diagnostics and development-flow checks, and `godoo-changeset-review` for
14
- whole-changeset reviews.
13
+ - Use `godoo-editor-validation` for VS Code diagnostics and development-flow checks, `godoo-changeset-review` for
14
+ whole-changeset reviews, and `godoo-release-management` for release preparation and publication.
15
15
  - Keep shared Docker validation sequential.
16
16
 
17
17
  ## Branches and releases
@@ -24,13 +24,6 @@ coherent change.
24
24
  Follow the existing merge strategy and commit conventions. Push and merge only within explicit authorization.
25
25
  - After merge, return to an updated `main` or start the next topic branch from current `origin/main`. Prune stale remote
26
26
  references and remove merged branches only after checking ancestry and worktree ownership; preserve unfinished work.
27
- - Leave package version changes to `.github/workflows/version-bump.yml`, dispatched against `main` with the chosen bump
28
- type. It creates `release/<version>` and the release PR; keep that PR limited to preparing the release.
29
- - Review the release PR description and available Quality results before merging: its description becomes the release
30
- notes. The release-labeled PR must target `main` and match the title, branch, and version identities documented in
31
- `.github/workflows/README.md`. Publication uses the release merge commit for PyPI, the tag, and the GitHub Release.
32
- - Keep release PRs short-lived and coordinate other merges to preserve the intended release scope. Do not use a
33
- long-lived `release` or `develop` branch for this workflow, or move an existing release tag to repair publication.
34
27
 
35
28
  ## Code and CLI
36
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gOdoo-cli
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Odoo lifecycle, Python dependency preflight, and shared Git workspaces.
5
5
  Project-URL: repository, https://github.com/OpenJKSoftware/gOdoo
6
6
  Author-email: Joshua Kreuder <Joshua_Kreuder@outlook.com>
@@ -7,18 +7,21 @@ Ordinary restores and major upgrades share the same initialization flow.
7
7
 
8
8
  ## Choose the right mechanism
9
9
 
10
- | Work | Mechanism | Runs when |
11
- | ----------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
12
- | Versioned schema or data repair | Addon `migrations/<version>/pre-*.py`, `post-*.py`, or `end-*.py` | An eligible installed-to-manifest version change occurs. |
13
- | External migration roots | Explicit `--upgrade-path` | Selected module updates invoke Odoo's loader. |
14
- | Cursor-based work before registry loading | Explicit `--pre-upgrade-script` | An update selection exists; maps to native `--pre-upgrade-scripts`. |
15
- | Choose modules to install/update | `prepare` hook | Each initialization attempt, before dependency preflight. |
16
- | Bootstrap or restore setup | `after_bootstrap` / `after_restore` hook | The matching initialization outcome applies. |
17
- | Recurring environment policy | `after_reconcile` hook | Reconciliation succeeds and declaration filters match. |
10
+ | Work | Mechanism | Mechanism owner | Runs when |
11
+ | ----------------------------------------- | ----------------------------------------------------------------- | --------------- | ------------------------------------------------------------------- |
12
+ | Versioned schema or data repair | Addon `migrations/<version>/pre-*.py`, `post-*.py`, or `end-*.py` | Odoo | An eligible installed-to-manifest version change occurs. |
13
+ | External migration roots | Explicit `--upgrade-path` | Odoo | Selected module updates invoke Odoo's loader. |
14
+ | Cursor-based work before registry loading | Explicit `--pre-upgrade-script` | Odoo | An update selection exists; maps to native `--pre-upgrade-scripts`. |
15
+ | Choose modules to install/update | `prepare` hook | gOdoo | Each initialization attempt, unless completed with `once=True`. |
16
+ | Bootstrap or restore setup | `after_bootstrap` / `after_restore` hook | gOdoo | The matching initialization outcome applies. |
17
+ | Recurring environment policy | `after_reconcile` hook | gOdoo | Reconciliation succeeds and declaration filters match. |
18
+
19
+ Projects supply the migration scripts and hook implementations. gOdoo forwards the selected native migration options to
20
+ Odoo and runs the lifecycle hooks.
18
21
 
19
22
  Native migration versions must be above the installed version and at or below the manifest version. Fresh installs need
20
23
  their own install behavior. There is no transition directory, version-pair selector, implicit module list, or hook
21
- implicit migration scheduler. Ordinary database hooks can opt into transactional one-shot completion with `once=True`.
24
+ implicit migration scheduler. Hooks can opt into one-shot completion with `once=True`.
22
25
 
23
26
  ## Discover and select hooks
24
27
 
@@ -49,7 +52,7 @@ actual boolean. A missing predicate runs whenever eligible; an exception fails i
49
52
  | `phase` | `prepare`, `after_bootstrap`, `after_restore`, or `after_reconcile`. |
50
53
  | `on` | Tuple of `ready`, `bootstrapped`, and/or `restored`; defaults to all. Custom outcome filters are invalid for preparation. |
51
54
  | `environments` | Optional tuple of exact `GODOO_ENVIRONMENT` values. Omitted means unrestricted; an allowlist never matches an unset environment. |
52
- | `once` | Optional boolean, default false. Record successful database-hook completion; preparation hooks cannot use it. |
55
+ | `once` | Optional boolean, default false. Record successful completion for this hook in the database. |
53
56
 
54
57
  Environment is a deployment input, never inferred from database contents.
55
58
 
@@ -67,10 +70,38 @@ def run(ctx):
67
70
  ctx.request_modules(update=["project_base"], install=["helpdesk"])
68
71
  ```
69
72
 
70
- Preparation runs without starting Odoo and recomputes requests on every retry. Its context exposes `environment` and
71
- `request_modules(update=..., install=...)`, with no `env`. Preparation must not mutate databases or external systems. No
72
- request means no implicit action; even `all` requires an explicit request. Upgrade paths and pre-upgrade scripts remain
73
- explicit CLI/environment inputs.
73
+ Preparation runs without starting Odoo. Its context exposes `environment` and
74
+ `request_modules(update=..., install=...)`, with no `env`. Pass module names as sequences of strings. Requests combine
75
+ with CLI and environment selections before dependency preflight and native reconciliation.
76
+
77
+ `install=[...]` in a recurring hook requests installation on each initialization. Modules already installed or queued
78
+ for upgrade are skipped; other requested modules are passed to Odoo for installation. A module uninstalled later is
79
+ requested again on the next initialization. Change or remove the request if that module should remain uninstalled.
80
+ Preparation must not mutate databases or external systems. No request means no implicit action; even `all` requires an
81
+ explicit request. Upgrade paths and pre-upgrade scripts remain explicit CLI/environment inputs.
82
+
83
+ A one-time preparation hook requests modules until reconciliation succeeds and every requested install is verified:
84
+
85
+ ```python
86
+ # hooks/11_install_feature.py
87
+ from godoo_cli.runtime.hooks import Hook
88
+
89
+ HOOK = Hook(id="project.install_feature", phase="prepare", once=True)
90
+
91
+
92
+ def run(ctx):
93
+ ctx.request_modules(install=["helpdesk"])
94
+ ```
95
+
96
+ Run `godoo runtime start --hooks-dir /path/to/hooks` to initialize and reconcile before launching Odoo. A standalone
97
+ `runtime launch` only starts a prepared runtime and does not run hooks. Once completed, the hook is skipped even if an
98
+ operator later uninstalls the module. Use a new hook ID for a new installation request. List module names explicitly;
99
+ `install=["all"]` cannot be verified and is rejected in one-time preparation hooks.
100
+
101
+ One-time preparation runs after bootstrap or restore, when gOdoo can read the database completion record, but before
102
+ other database hooks and native reconciliation. Its dependencies are therefore checked after bootstrap or restore. If
103
+ reconciliation or verification fails, the hook runs again on retry using its current code. Native Odoo work and the
104
+ completion record are separate transactions, so an interruption after native work may cause a retry.
74
105
 
75
106
  ## Reconcile database state
76
107
 
@@ -154,15 +185,17 @@ retries; it is not a completion record.
154
185
 
155
186
  Use `Hook(id="project.repair.2026_10", phase="after_reconcile", once=True)` for a transactional one-shot repair. IDs are
156
187
  immutable: use a new ID for a new repair. The runner stores completion in `ir_config_parameter` under
157
- `godoo.hooks.once.<identity-hash>`, in the same transaction as the action. Unrestricted hooks complete globally in that
158
- database; environment-restricted hooks complete separately for each selected environment. Restores and baseline resets
159
- carry or rewind the records with the database. Declaration filters still apply, and duplicate IDs remain invalid.
160
-
161
- Completed hooks skip `needs_run`. A false predicate means the desired state is already satisfied and records completion
162
- atomically without running the action. An exception rolls back the action and its completion record. A later hook's
163
- failure preserves earlier committed repairs. Hooks that commit internally or change external systems still need their
164
- own idempotency: the runner cannot promise exactly-once external effects. Keep recurring environment policy repeatable,
165
- with state predicates, so ordinary drift is repaired after every controlled start.
188
+ `godoo.hooks.once.<identity-hash>`. For database hooks, it is stored in the same transaction as the action. Unrestricted
189
+ hooks complete globally in that database; environment-restricted hooks complete separately for each selected
190
+ environment. Restores and baseline resets carry or rewind the records with the database. Declaration filters still
191
+ apply, and duplicate IDs remain invalid.
192
+
193
+ Completed hooks skip `needs_run`. For database hooks, a false predicate records completion atomically without running
194
+ the action; an exception rolls back the action and its completion record. For preparation hooks, completion is recorded
195
+ after successful reconciliation and verification. A later hook's failure preserves earlier committed repairs. Hooks that
196
+ commit internally or change external systems still need their own idempotency: the runner cannot promise exactly-once
197
+ external effects. Keep recurring environment policy repeatable, with state predicates, so ordinary drift is repaired
198
+ after every controlled start.
166
199
 
167
200
  | Attempt | Outcome visible to hooks |
168
201
  | ---------------------------------------------- | ----------------------------------------------------------------------------------- |
@@ -15,7 +15,7 @@ cd /path/to/project
15
15
  # Set this in .env instead if each shell should inherit the same value.
16
16
  export GODOO_SOURCES_ROOT=/absolute/path/to/shared-sources
17
17
 
18
- # Synchronize managed sources. This can discard edits in verified worktrees.
18
+ # Synchronize managed sources. Tracked edits and untracked path collisions stop synchronization.
19
19
  godoo workspace sync
20
20
 
21
21
  # Generate editor files from the selected sources.
@@ -62,18 +62,19 @@ flowchart TD
62
62
  Check --> Build["Production image build copies<br/>selected sources into the image"]
63
63
  ```
64
64
 
65
- | Command | Changes state? | Purpose |
66
- | ----------------------- | -------------- | -------------------------------------------------------------------------- |
67
- | `workspace sync` | Yes | Update managed worktrees and archive caches; may reset verified worktrees. |
68
- | `workspace configure` | Yes | Validate sources and generate editor files under `.godoo/workspace.lock`. |
69
- | `workspace check` | No | Validate offline, without source-worktree or archive locks. |
70
- | `workspace runtime-env` | No | Validate sources and emit shell-safe resolved path assignments. |
71
- | `workspace materialize` | Yes | Copy selected sources into an image layout. |
72
- | `workspace storage` | No | Inspect shared source storage. |
73
- | `workspace gc` | Yes | Manage shared storage cleanup; inspect `--help` before use. |
65
+ | Command | Changes state? | Purpose |
66
+ | ----------------------- | -------------- | ------------------------------------------------------------------------- |
67
+ | `workspace sync` | Yes | Update managed worktrees and archive caches; preserve untracked files. |
68
+ | `workspace configure` | Yes | Validate sources and generate editor files under `.godoo/workspace.lock`. |
69
+ | `workspace check` | No | Validate offline, without source-worktree or archive locks. |
70
+ | `workspace runtime-env` | No | Validate sources and emit shell-safe resolved path assignments. |
71
+ | `workspace materialize` | Yes | Copy selected sources into an image layout. |
72
+ | `workspace storage` | No | Inspect shared source storage. |
73
+ | `workspace gc` | Yes | Manage shared storage cleanup; inspect `--help` before use. |
74
74
 
75
75
  Git fetches report elapsed time every ten seconds and retain Git output for failure diagnostics. Git owns the pools,
76
- worktrees, branches, locks, and caches under the shared source root; use workspace commands to manage them.
76
+ worktrees, branches, locks, and caches under the shared source root; use workspace commands to manage them. Tracked
77
+ edits stop synchronization. Untracked and ignored files are preserved; Git rejects updates that would overwrite them.
77
78
 
78
79
  ## Editor configuration
79
80
 
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "1.0.4"
3
+ __version__ = "1.0.6"
@@ -13,9 +13,21 @@ import typer
13
13
 
14
14
  from ...models import GodooConfig
15
15
  from ...runtime.archive import load_runtime_archive, prepare_archive_with_original_filestore
16
- from ...runtime.checksums import ModulePlan, module_states, pending_installs, plan_module_changes
16
+ from ...runtime.checksums import (
17
+ ModulePlan,
18
+ module_states,
19
+ pending_installs,
20
+ plan_module_changes,
21
+ verify_installed_modules,
22
+ )
17
23
  from ...runtime.dataset import resolve_runtime_dataset
18
- from ...runtime.hooks import discover_hooks, has_eligible_hooks, run_prepare_hooks
24
+ from ...runtime.hooks import (
25
+ complete_prepare_hooks,
26
+ discover_hooks,
27
+ has_eligible_hooks,
28
+ pending_prepare_hooks,
29
+ run_prepare_hooks,
30
+ )
19
31
  from ...runtime.lifecycle import (
20
32
  LifecycleBootstrapError,
21
33
  deployment_init,
@@ -423,10 +435,35 @@ def initialize_odoo_runtime(resolve_config: Callable[[], GodooConfig], request:
423
435
  plan: ModulePlan | None = None
424
436
  selected_updates = split_lifecycle_values(update_modules)
425
437
  selected_installs = split_lifecycle_values(install_modules)
438
+ requested_installs = list(selected_installs)
439
+ prepare_completion_keys: list[str] = []
426
440
 
427
441
  def plan_reconciliation(conf: GodooConfig) -> None:
428
442
  """Select native work under the lifecycle lock before any database hook."""
429
- nonlocal plan, selected_updates, selected_installs
443
+ nonlocal plan, selected_updates, selected_installs, requested_installs, prepare_completion_keys
444
+ once_hooks, prepare_completion_keys = pending_prepare_hooks(conf, hooks, environment)
445
+ if once_hooks:
446
+ once_modules = run_prepare_hooks(once_hooks, environment, once=True)
447
+ once_updates = split_lifecycle_values(list(once_modules.update))
448
+ once_installs = split_lifecycle_values(list(once_modules.install))
449
+ if "all" in once_installs:
450
+ message = "One-time preparation hooks must name install modules explicitly; 'all' cannot be verified."
451
+ raise ValueError(message)
452
+ selected_updates = list(dict.fromkeys([*selected_updates, *once_updates]))
453
+ selected_installs = list(dict.fromkeys([*selected_installs, *once_installs]))
454
+ requested_installs = list(selected_installs)
455
+ _validate_pre_upgrade_options(
456
+ runtime_seed is not None or (persisted is not None and persisted["outcome"] == "restored"),
457
+ pre_upgrade_scripts,
458
+ selected_updates,
459
+ has_eligible_hooks(hooks, phase="after_restore", outcome="restored", environment=environment),
460
+ )
461
+ preflight_reconcile_dependencies(
462
+ conf,
463
+ selected_updates,
464
+ selected_installs,
465
+ ignore_missing_installed_modules=bool(pre_upgrade_scripts),
466
+ )
430
467
  if request.update_changed:
431
468
  plan = plan_module_changes(
432
469
  conf, selected_updates, selected_installs, pre_upgrade_scripts=bool(pre_upgrade_scripts)
@@ -452,6 +489,10 @@ def initialize_odoo_runtime(resolve_config: Callable[[], GodooConfig], request:
452
489
  )
453
490
  if result == 0 and plan is not None:
454
491
  plan.finish(conf)
492
+ elif result == 0 and requested_installs:
493
+ verify_installed_modules(conf, requested_installs)
494
+ if result == 0:
495
+ complete_prepare_hooks(conf, prepare_completion_keys)
455
496
  return result
456
497
 
457
498
  try:
@@ -480,7 +521,7 @@ def initialize_odoo_runtime(resolve_config: Callable[[], GodooConfig], request:
480
521
  except LifecycleBootstrapError as error:
481
522
  LOGGER.exception("Native Odoo bootstrap failed during deployment initialization")
482
523
  return error.return_code
483
- except (ValueError, RuntimeError, OSError, psycopg2.Error):
524
+ except (TypeError, ValueError, RuntimeError, OSError, psycopg2.Error):
484
525
  LOGGER.exception("Deployment initialization failed")
485
526
  return 1
486
527
  finally:
@@ -129,22 +129,6 @@ class GitRepository:
129
129
  """Return Git common directory for this repository."""
130
130
  return Path(self._require_repo().git.rev_parse("--git-common-dir"))
131
131
 
132
- def switch(self, branch: str, start: str, *, discard_changes: bool = False) -> None:
133
- """Switch an existing worktree branch to a commit."""
134
- arguments: list[str] = []
135
- if discard_changes:
136
- arguments.append("--discard-changes")
137
- arguments.extend(["-C", branch, start])
138
- self._require_repo().git.switch(*arguments)
139
-
140
- def reset_hard(self, commit: str) -> None:
141
- """Reset the current worktree to commit."""
142
- self._require_repo().git.reset("--hard", commit)
143
-
144
- def clean(self) -> None:
145
- """Remove untracked files from the current worktree."""
146
- self._require_repo().git.clean("-fdx")
147
-
148
132
 
149
133
  def repository(path: Path) -> GitRepository:
150
134
  """Open a repository through the shared facade."""
@@ -8,7 +8,7 @@ import logging
8
8
  import os
9
9
  import re
10
10
  from collections.abc import Container, Sequence
11
- from dataclasses import dataclass
11
+ from dataclasses import dataclass, field
12
12
  from pathlib import Path
13
13
 
14
14
  from ..helpers.hashing import fingerprint, sha256_file
@@ -50,6 +50,21 @@ def pending_installs(states: dict[str, ModuleState], requested: list[str]) -> li
50
50
  )
51
51
 
52
52
 
53
+ def verify_installed_modules(config: GodooConfig, requested: list[str]) -> None:
54
+ """Require every explicitly requested module to be installed after reconciliation."""
55
+ states = module_states(config)
56
+ missing = _missing_installs(states, requested)
57
+ if missing:
58
+ message = f"Requested modules were not installed: {', '.join(missing)}"
59
+ raise RuntimeError(message)
60
+
61
+
62
+ def _missing_installs(states: dict[str, ModuleState], requested: list[str]) -> list[str]:
63
+ return sorted(
64
+ name for name in requested if name != "all" and states.get(name, ModuleState("unknown")).state != "installed"
65
+ )
66
+
67
+
53
68
  def _tree_hash(path: Path, *, core: bool = False) -> str:
54
69
  files = []
55
70
  for directory, folders, names in os.walk(path):
@@ -141,6 +156,7 @@ class ModulePlan:
141
156
  updates: list[str]
142
157
  installs: list[str]
143
158
  snapshot: dict[str, str]
159
+ requested_installs: list[str] = field(default_factory=list)
144
160
 
145
161
  def finish(self, config: GodooConfig) -> None:
146
162
  """Verify native completion and unchanged sources before saving this snapshot."""
@@ -149,11 +165,7 @@ class ModulePlan:
149
165
  if pending:
150
166
  message = f"Native reconciliation left unfinished modules: {', '.join(pending)}"
151
167
  raise RuntimeError(message)
152
- missing_installs = sorted(
153
- name
154
- for name in self.installs
155
- if name != "all" and states.get(name, ModuleState("unknown")).state != "installed"
156
- )
168
+ missing_installs = _missing_installs(states, [*self.installs, *self.requested_installs])
157
169
  if missing_installs:
158
170
  message = f"Requested modules were not installed: {', '.join(missing_installs)}"
159
171
  raise RuntimeError(message)
@@ -217,4 +229,4 @@ def plan_module_changes(
217
229
  )
218
230
  raise RuntimeError(message)
219
231
  LOGGER.info("Source reconciliation selected updates=%s installs=%s", selected_updates, selected_installs)
220
- return ModulePlan(selected_updates, selected_installs, snapshot)
232
+ return ModulePlan(selected_updates, selected_installs, snapshot, requested_installs=list(installs))
@@ -45,9 +45,6 @@ class Hook:
45
45
  if type(self.once) is not bool:
46
46
  message = "Hook once must be a boolean."
47
47
  raise ValueError(message)
48
- if self.phase == "prepare" and self.once:
49
- message = "Preparation hooks cannot declare once=True."
50
- raise ValueError(message)
51
48
  if self.phase == "prepare" and self.on != OUTCOMES:
52
49
  message = "Preparation hooks cannot filter by outcome."
53
50
  raise ValueError(message)
@@ -131,10 +128,14 @@ def has_eligible_hooks(hooks: Iterable[HookDefinition], *, phase: str, outcome:
131
128
  return any(_is_eligible(definition.hook, phase, outcome, environment) for definition in hooks)
132
129
 
133
130
 
134
- def run_prepare_hooks(hooks: Iterable[HookDefinition], environment: str | None) -> ModuleSelections:
131
+ def run_prepare_hooks(
132
+ hooks: Iterable[HookDefinition], environment: str | None, *, once: bool = False
133
+ ) -> ModuleSelections:
135
134
  """Run eligible preparation hooks and collect their module requests."""
136
135
  context = _PrepareContext(environment=environment)
137
136
  for definition in hooks:
137
+ if definition.hook.once != once:
138
+ continue
138
139
  if not _is_eligible(definition.hook, "prepare", None, environment):
139
140
  LOGGER.info("Hook %s excluded by declaration.", definition.hook.id)
140
141
  continue
@@ -142,6 +143,46 @@ def run_prepare_hooks(hooks: Iterable[HookDefinition], environment: str | None)
142
143
  return ModuleSelections(update=_deduplicate(context._update), install=_deduplicate(context._install))
143
144
 
144
145
 
146
+ def _completion_key(hook: Hook, environment: str | None) -> str:
147
+ scope = environment if hook.environments is not None else None
148
+ identity = json.dumps([hook.id, scope], separators=(",", ":"))
149
+ return "godoo.hooks.once." + hashlib.sha256(identity.encode()).hexdigest()
150
+
151
+
152
+ def pending_prepare_hooks(
153
+ config: GodooConfig, hooks: Iterable[HookDefinition], environment: str | None
154
+ ) -> tuple[list[HookDefinition], list[str]]:
155
+ """Select eligible one-time preparation hooks not yet completed in this database."""
156
+ selected = [
157
+ definition
158
+ for definition in hooks
159
+ if definition.hook.once and _is_eligible(definition.hook, "prepare", None, environment)
160
+ ]
161
+ if not selected:
162
+ return [], []
163
+ keys = [_completion_key(definition.hook, environment) for definition in selected]
164
+ with config.db_connection.with_db(config.db_name, readonly=True).connect() as cursor:
165
+ cursor.execute(
166
+ "SELECT key FROM ir_config_parameter WHERE key = ANY(%s) AND value IS NOT NULL AND value != ''", [keys]
167
+ )
168
+ completed = {row[0] for row in cursor.fetchall()}
169
+ pending = [(definition, key) for definition, key in zip(selected, keys, strict=True) if key not in completed]
170
+ return [definition for definition, _key in pending], [key for _definition, key in pending]
171
+
172
+
173
+ def complete_prepare_hooks(config: GodooConfig, keys: list[str]) -> None:
174
+ """Record verified one-time preparation hooks after native reconciliation."""
175
+ if not keys:
176
+ return
177
+ with config.db_connection.connect() as cursor:
178
+ for key in keys:
179
+ cursor.execute(
180
+ "INSERT INTO ir_config_parameter (key, value) VALUES (%s, %s) "
181
+ "ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
182
+ [key, "1"],
183
+ )
184
+
185
+
145
186
  def run_hook_phase(
146
187
  config: GodooConfig,
147
188
  hooks: Iterable[HookDefinition],
@@ -188,9 +229,7 @@ def _run_database_hook_batch(shell_env: Any, sources: Sequence[str], *, outcome:
188
229
  completion_key = None
189
230
  parameters = None
190
231
  if definition.hook.once:
191
- scope = environment if definition.hook.environments is not None else None
192
- identity = json.dumps([definition.hook.id, scope], separators=(",", ":"))
193
- completion_key = "godoo.hooks.once." + hashlib.sha256(identity.encode()).hexdigest()
232
+ completion_key = _completion_key(definition.hook, environment)
194
233
  parameters = context.env["ir.config_parameter"].sudo()
195
234
  if parameters.get_param(completion_key):
196
235
  cursor.rollback()
@@ -68,7 +68,7 @@ def _status_result(
68
68
  cwd: Path | None = None,
69
69
  env: dict[str, str] | None = None,
70
70
  ) -> tuple[int, str | bytes, str | bytes]:
71
- """Run ignored status in porcelain NUL mode for the clean-tree safety check."""
71
+ """Run status in porcelain NUL mode for worktree safety checks."""
72
72
  return native_git_result(*arguments, cwd=cwd, env=env, binary=True)
73
73
 
74
74
 
@@ -444,36 +444,18 @@ def _head(path: Path) -> str:
444
444
  return _repo(path).head_commit()
445
445
 
446
446
 
447
- def _assert_clean(path: Path) -> None:
448
- """Reject any direct change to a managed worktree."""
447
+ def _assert_clean(path: Path, *, include_untracked: bool = False) -> None:
448
+ """Reject tracked changes, and all untracked files before removing a worktree."""
449
+ arguments = ["--untracked-files=all", "--ignored"] if include_untracked else ["--untracked-files=no"]
449
450
  returncode, stdout, stderr = _status_result(
450
- "--no-optional-locks",
451
- "-C",
452
- str(path),
453
- "status",
454
- "--porcelain=v1",
455
- "-z",
456
- "--untracked-files=all",
457
- "--ignored",
451
+ "--no-optional-locks", "-C", str(path), "status", "--porcelain=v1", "-z", *arguments
458
452
  )
459
453
  if returncode:
460
454
  detail = stderr.strip() or stdout.strip()
461
455
  if isinstance(detail, bytes):
462
456
  detail = detail.decode(errors="replace")
463
457
  workspace_fail(f"Git status failed for {path}: {detail}")
464
- entries = os.fsdecode(stdout).split("\0")
465
- unexpected = [
466
- entry
467
- for entry in entries
468
- if entry
469
- and not (
470
- entry.startswith("!! ")
471
- and (candidate := path / entry[3:]).name == ".DS_Store"
472
- and candidate.is_file()
473
- and not candidate.is_symlink()
474
- )
475
- ]
476
- if unexpected:
458
+ if stdout:
477
459
  workspace_fail(f"Managed source is dirty; direct edits are unsupported: {path}")
478
460
 
479
461
 
@@ -542,6 +524,13 @@ def _checkout_environment() -> dict[str, str]:
542
524
  }
543
525
 
544
526
 
527
+ def _reset_worktree(path: Path, commit: str) -> None:
528
+ """Restore tracked files without applying host Git configuration."""
529
+ _worktree_command(
530
+ "-C", str(path), "-c", "core.autocrlf=false", "reset", "--hard", commit, env=_checkout_environment()
531
+ )
532
+
533
+
545
534
  def _merge_commit(path: Path, tree: str, previous: str, commit: str) -> str:
546
535
  """Write the deterministic commit used for one ordered merge input."""
547
536
  environment = _checkout_environment()
@@ -564,10 +553,47 @@ def _merge_commit(path: Path, tree: str, previous: str, commit: str) -> str:
564
553
  )
565
554
 
566
555
 
556
+ def _assert_no_ignored_merge_collisions(path: Path, commit: str) -> None:
557
+ """Reject ignored files that an incoming merge could overwrite."""
558
+
559
+ def names(*arguments: str) -> set[str]:
560
+ returncode, stdout, stderr = _status_result("-C", str(path), *arguments, "-z")
561
+ if returncode:
562
+ workspace_fail(f"Git path inspection failed for {path}: {os.fsdecode(stderr).strip()}")
563
+ return set(os.fsdecode(stdout).split("\0")) - {""}
564
+
565
+ ignored = names("ls-files", "--others", "--ignored", "--exclude-standard")
566
+ if not ignored:
567
+ return
568
+ tree = _merge_command(
569
+ "-C",
570
+ str(path),
571
+ "-c",
572
+ "merge.renormalize=false",
573
+ "-c",
574
+ "rerere.enabled=false",
575
+ "merge-tree",
576
+ "--write-tree",
577
+ "HEAD",
578
+ commit,
579
+ env=_checkout_environment(),
580
+ ).splitlines()[0]
581
+ incoming = names("ls-tree", "-r", "--name-only", tree)
582
+ for name in incoming:
583
+ parts = name.split("/")
584
+ if any("/".join(parts[:index]) in ignored for index in range(1, len(parts) + 1)):
585
+ workspace_fail(f"Ignored file would be overwritten by merge in {path}: {name}")
586
+ for name in ignored:
587
+ parts = name.split("/")
588
+ if any("/".join(parts[:index]) in incoming for index in range(1, len(parts) + 1)):
589
+ workspace_fail(f"Ignored file would be overwritten by merge in {path}: {name}")
590
+
591
+
567
592
  def _merge_sources(path: Path, merge_commits: tuple[str, ...]) -> str:
568
593
  """Merge ordered inputs using fixed configuration and reproducible commits."""
569
594
  environment = _checkout_environment()
570
595
  for commit in merge_commits:
596
+ _assert_no_ignored_merge_collisions(path, commit)
571
597
  previous = _head(path)
572
598
  _merge_command(
573
599
  "-C",
@@ -586,6 +612,7 @@ def _merge_sources(path: Path, merge_commits: tuple[str, ...]) -> str:
586
612
  "--no-commit",
587
613
  "--no-stat",
588
614
  "--no-edit",
615
+ "--no-overwrite-ignore",
589
616
  "--",
590
617
  commit,
591
618
  env=environment,
@@ -656,7 +683,7 @@ def _publish_worktree(
656
683
  occupant = next((item for item in records if Path(item.get("worktree", "")) == path), None)
657
684
  if occupant is not None:
658
685
  _usable_worktree(pool, path, root)
659
- _assert_clean(path)
686
+ _assert_clean(path, include_untracked=True)
660
687
  _worktree_command("-C", str(pool), "worktree", "remove", str(path))
661
688
  elif path.exists() or path.is_symlink():
662
689
  workspace_fail(f"Readable source worktree path is occupied: {path}")
@@ -668,16 +695,26 @@ def _publish_worktree(
668
695
  workspace_fail(
669
696
  f"Managed worktree path is occupied by another branch: {path} ({record.get('branch', 'detached')})."
670
697
  )
698
+ _assert_clean(selected)
671
699
  previous = _head(selected)
672
- if record.get("branch") != branch_ref:
673
- _repo(selected).switch(worktree_branch, base_commit, discard_changes=True)
674
- _repo(selected).reset_hard(base_commit)
675
- _repo(selected).clean()
700
+ _worktree_command(
701
+ "-C",
702
+ str(selected),
703
+ "-c",
704
+ "core.hooksPath=/dev/null",
705
+ "-c",
706
+ "core.autocrlf=false",
707
+ "switch",
708
+ "--no-overwrite-ignore",
709
+ "-C",
710
+ worktree_branch,
711
+ base_commit,
712
+ env=_checkout_environment(),
713
+ )
676
714
  try:
677
715
  resolved = _merge_sources(selected, merge_commits) if merge_commits else _head(selected)
678
716
  except Exception:
679
- _repo(selected).reset_hard(previous)
680
- _repo(selected).clean()
717
+ _reset_worktree(selected, previous)
681
718
  raise
682
719
  return selected, resolved
683
720
 
@@ -731,8 +768,7 @@ def _create_worktree(
731
768
  )
732
769
  created_branch = not existing
733
770
  if existing:
734
- _repo(staging).reset_hard(base_commit)
735
- _repo(staging).clean()
771
+ _reset_worktree(staging, base_commit)
736
772
  resolved = _merge_sources(staging, merge_commits) if merge_commits else _head(staging)
737
773
  _assert_clean(staging)
738
774
  _worktree_command(