half-orm-dev 0.16.0a9__tar.gz → 0.17.0a1__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 (65) hide show
  1. {half_orm_dev-0.16.0a9/half_orm_dev.egg-info → half_orm_dev-0.17.0a1}/PKG-INFO +3 -3
  2. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/README.md +1 -1
  3. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/init.py +1 -1
  4. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/database.py +1 -1
  5. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patch_manager.py +22 -13
  6. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/release_manager.py +146 -116
  7. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/repo.py +9 -1
  8. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/conftest_template +8 -3
  9. half_orm_dev-0.17.0a1/half_orm_dev/templates/pre-commit +59 -0
  10. half_orm_dev-0.17.0a1/half_orm_dev/version.txt +1 -0
  11. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1/half_orm_dev.egg-info}/PKG-INFO +3 -3
  12. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  13. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev.egg-info/requires.txt +1 -1
  14. half_orm_dev-0.16.0a9/half_orm_dev/version.txt +0 -1
  15. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/AUTHORS +0 -0
  16. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/LICENSE +0 -0
  17. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/__init__.py +0 -0
  18. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/__init__.py +0 -0
  19. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/__init__.py +0 -0
  20. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/apply.py +0 -0
  21. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/clone.py +0 -0
  22. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/new.py +0 -0
  23. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/patch.py +0 -0
  24. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/prepare.py +0 -0
  25. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/prepare_release.py +0 -0
  26. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/promote_to.py +0 -0
  27. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/release.py +0 -0
  28. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/restore.py +0 -0
  29. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/sync.py +0 -0
  30. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/todo.py +0 -0
  31. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/undo.py +0 -0
  32. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/update.py +0 -0
  33. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/commands/upgrade.py +0 -0
  34. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli/main.py +0 -0
  35. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/cli_extension.py +0 -0
  36. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/hgit.py +0 -0
  37. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/hop.py +0 -0
  38. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/manifest.py +0 -0
  39. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/modules.py +0 -0
  40. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patch.py +0 -0
  41. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patch_validator.py +0 -0
  42. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  43. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  44. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  45. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patches/log +0 -0
  46. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  47. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/.gitignore +0 -0
  48. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/MANIFEST.in +0 -0
  49. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/Pipfile +0 -0
  50. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/README +0 -0
  51. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/init_module_template +0 -0
  52. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/module_template_1 +0 -0
  53. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/module_template_2 +0 -0
  54. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/module_template_3 +0 -0
  55. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/relation_test +0 -0
  56. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/setup.py +0 -0
  57. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/sql_adapter +0 -0
  58. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/templates/warning +0 -0
  59. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev/utils.py +0 -0
  60. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  61. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/half_orm_dev.egg-info/top_level.txt +0 -0
  62. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/setup.cfg +0 -0
  63. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/setup.py +0 -0
  64. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/tests/__init__.py +0 -0
  65. {half_orm_dev-0.16.0a9 → half_orm_dev-0.17.0a1}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.16.0a9
3
+ Version: 0.17.0a1
4
4
  Summary: half_orm development Framework.
5
5
  Home-page: https://github.com/collorg/halfORM_dev
6
6
  Author: Joël Maïzi
@@ -24,7 +24,7 @@ License-File: AUTHORS
24
24
  Requires-Dist: GitPython
25
25
  Requires-Dist: click
26
26
  Requires-Dist: pydash
27
- Requires-Dist: half_orm<0.17.0,>=0.16.0
27
+ Requires-Dist: half_orm<0.18.0,>=0.17.0
28
28
  Requires-Dist: pytest
29
29
  Dynamic: author
30
30
  Dynamic: author-email
@@ -927,7 +927,7 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
927
927
 
928
928
  ---
929
929
 
930
- **Version**: 0.16.0
930
+ **Version**: 0.17.0
931
931
  **halfORM**: Compatible with halfORM 0.16.x
932
932
  **Python**: 3.8+
933
933
  **PostgreSQL**: Tested with 13+ (might work with earlier versions)
@@ -887,7 +887,7 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
887
887
 
888
888
  ---
889
889
 
890
- **Version**: 0.16.0
890
+ **Version**: 0.17.0
891
891
  **halfORM**: Compatible with halfORM 0.16.x
892
892
  **Python**: 3.8+
893
893
  **PostgreSQL**: Tested with 13+ (might work with earlier versions)
@@ -33,7 +33,7 @@ class ProjectDirectoryExistsError(Exception):
33
33
  @click.option('--production', is_flag=True, help='Mark as production environment (default: False)')
34
34
  @click.option('--force-sync-only', is_flag=True, help='Skip metadata installation, force sync-only mode')
35
35
  @click.option('--create-db', is_flag=False, default=True)
36
- @click.option('--docker', default=None, help='Docker container name for PostgreSQL')
36
+ @click.option('--docker', default='', help='Docker container name for PostgreSQL')
37
37
  def init(project_name, host, port, user, password, git_origin, production, force_sync_only, create_db, docker):
38
38
  """
39
39
  Initialize a new half_orm_dev project with database and code structure.
@@ -820,7 +820,7 @@ class Database:
820
820
  ... )
821
821
  """
822
822
  # Detect execution mode based on docker_container presence
823
- docker_container = connection_params.get('docker_container', '')
823
+ docker_container = connection_params.get('docker_container')
824
824
 
825
825
  if docker_container:
826
826
  # Docker mode: Execute command inside Docker container
@@ -1235,16 +1235,18 @@ class PatchManager:
1235
1235
  2. Validates repository is clean
1236
1236
  3. Validates git remote is configured
1237
1237
  4. Validates and normalizes patch ID format
1238
- 5. Fetches all references from remote (branches + tags)
1239
- 5.5 Validates ho-prod is synced with origin/ho-prod (NEW)
1240
- 6. Checks patch number available via tag lookup
1241
- 7. Creates Patches/PATCH_ID/ directory (on ho-prod)
1242
- 8. Commits directory on ho-prod "Add Patches/{patch_id} directory"
1243
- 9. Creates local tag ho-patch/{number} (points to commit on ho-prod)
1244
- 10. **Pushes tag to reserve number globally** POINT OF NO RETURN
1245
- 11. Creates ho-patch/PATCH_ID branch from current commit
1246
- 12. Pushes branch to remote (with retry)
1247
- 13. Checkouts to new patch branch
1238
+ 5. **ACQUIRES DISTRIBUTED LOCK on ho-prod** (30min timeout)
1239
+ 6. Fetches all references from remote (branches + tags) - with lock
1240
+ 6.5 Validates ho-prod is synced with origin/ho-prod
1241
+ 7. Checks patch number available via tag lookup (with up-to-date state)
1242
+ 8. Creates Patches/PATCH_ID/ directory (on ho-prod)
1243
+ 9. Commits directory on ho-prod "Add Patches/{patch_id} directory"
1244
+ 10. Creates local tag ho-patch/{number} (points to commit on ho-prod)
1245
+ 11. **Pushes tag to reserve number globally** ← POINT OF NO RETURN
1246
+ 12. Creates ho-patch/PATCH_ID branch from current commit
1247
+ 13. Pushes branch to remote (with retry)
1248
+ 14. **RELEASES LOCK** (always, even on error)
1249
+ 15. Checkouts to new patch branch
1248
1250
 
1249
1251
  Transactional guarantees:
1250
1252
  - Failure before step 10 (tag push): Complete rollback to initial state
@@ -1291,13 +1293,16 @@ class PatchManager:
1291
1293
  except Exception as e:
1292
1294
  raise PatchManagerError(f"Invalid patch ID: {e}")
1293
1295
 
1294
- # Step 5: Fetch all references from remote (branches + tags)
1296
+ # Step 5: ACQUIRE LOCK on ho-prod (with 30 min timeout for stale locks)
1297
+ lock_tag = self._repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
1298
+
1299
+ # Step 6: Fetch all references from remote (branches + tags) - with lock held
1295
1300
  self._fetch_from_remote()
1296
1301
 
1297
- # Step 5.5: Validate ho-prod is synced with origin (NEW)
1302
+ # Step 6.5: Validate ho-prod is synced with origin
1298
1303
  self._validate_ho_prod_synced_with_origin()
1299
1304
 
1300
- # Step 6: Check patch number available (via tag)
1305
+ # Step 7: Check patch number available (via tag, with up-to-date state)
1301
1306
  branch_name = f"ho-patch/{normalized_id}"
1302
1307
  self._check_patch_id_available(normalized_id)
1303
1308
 
@@ -1360,6 +1365,10 @@ class PatchManager:
1360
1365
  )
1361
1366
  raise PatchManagerError(f"Patch creation failed: {e}")
1362
1367
 
1368
+ finally:
1369
+ # ALWAYS release lock (even on error)
1370
+ self._repo.hgit.release_branch_lock(lock_tag)
1371
+
1363
1372
  # Step 13: Checkout to new branch (non-critical, warn if fails)
1364
1373
  try:
1365
1374
  self._checkout_branch(branch_name)
@@ -143,6 +143,7 @@ class ReleaseManager:
143
143
  origin, and pushes to reserve version globally.
144
144
 
145
145
  Workflow:
146
+ 0. Acquire lock tag
146
147
  1. Validate on ho-prod branch
147
148
  2. Validate repository is clean
148
149
  3. Fetch from origin
@@ -153,6 +154,7 @@ class ReleaseManager:
153
154
  8. Create empty stage file
154
155
  9. Commit with message "Prepare release X.Y.Z-stage"
155
156
  10. Push to origin (global reservation)
157
+ 11. Release lock tag
156
158
 
157
159
  Branch requirements:
158
160
  - Must be on ho-prod branch
@@ -197,76 +199,84 @@ class ReleaseManager:
197
199
  except ReleaseManagerError as e:
198
200
  print(f"Failed: {e}")
199
201
  """
200
- # 1. Validate on ho-prod branch
201
- if self._repo.hgit.branch != 'ho-prod':
202
- raise ReleaseManagerError(
203
- f"Must be on ho-prod branch to prepare release.\n"
204
- f"Current branch: {self._repo.hgit.branch}\n"
205
- f"Switch to ho-prod: git checkout ho-prod"
206
- )
202
+ try:
203
+ # 0. ACQUIRE LOCK on ho-prod (with 30 min timeout for stale locks)
204
+ lock_tag = self._repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
207
205
 
208
- # 2. Validate repository is clean
209
- if not self._repo.hgit.repos_is_clean():
210
- raise ReleaseManagerError(
211
- "Repository has uncommitted changes.\n"
212
- "Commit or stash changes before preparing release:\n"
213
- " git status\n"
214
- " git add . && git commit"
215
- )
206
+ # 1. Validate on ho-prod branch
207
+ if self._repo.hgit.branch != 'ho-prod':
208
+ raise ReleaseManagerError(
209
+ f"Must be on ho-prod branch to prepare release.\n"
210
+ f"Current branch: {self._repo.hgit.branch}\n"
211
+ f"Switch to ho-prod: git checkout ho-prod"
212
+ )
216
213
 
217
- # 3. Fetch from origin
218
- self._repo.hgit.fetch_from_origin()
214
+ # 2. Validate repository is clean
215
+ if not self._repo.hgit.repos_is_clean():
216
+ raise ReleaseManagerError(
217
+ "Repository has uncommitted changes.\n"
218
+ "Commit or stash changes before preparing release:\n"
219
+ " git status\n"
220
+ " git add . && git commit"
221
+ )
219
222
 
220
- # 4. Synchronize with origin
221
- is_synced, status = self._repo.hgit.is_branch_synced("ho-prod")
223
+ # 3. Fetch from origin
224
+ self._repo.hgit.fetch_from_origin()
222
225
 
223
- if status == "behind":
224
- # Pull automatically
225
- self._repo.hgit.pull()
226
- elif status == "diverged":
227
- raise ReleaseManagerError(
228
- "ho-prod has diverged from origin/ho-prod.\n"
229
- "Manual resolution required:\n"
230
- " git pull --rebase origin ho-prod\n"
231
- " or\n"
232
- " git merge origin/ho-prod"
233
- )
234
- # If "synced" or "ahead", continue
226
+ # 4. Synchronize with origin
227
+ is_synced, status = self._repo.hgit.is_branch_synced("ho-prod")
228
+
229
+ if status == "behind":
230
+ # Pull automatically
231
+ self._repo.hgit.pull()
232
+ elif status == "diverged":
233
+ raise ReleaseManagerError(
234
+ "ho-prod has diverged from origin/ho-prod.\n"
235
+ "Manual resolution required:\n"
236
+ " git pull --rebase origin ho-prod\n"
237
+ " or\n"
238
+ " git merge origin/ho-prod"
239
+ )
240
+ # If "synced" or "ahead", continue
235
241
 
236
- # 5. Read production version from model/schema.sql
237
- prod_version_str = self._get_production_version()
242
+ # 5. Read production version from model/schema.sql
243
+ prod_version_str = self._get_production_version()
238
244
 
239
- # Parse into Version object for calculation
240
- prod_version = self.parse_version_from_filename(f"{prod_version_str}.txt")
245
+ # Parse into Version object for calculation
246
+ prod_version = self.parse_version_from_filename(f"{prod_version_str}.txt")
241
247
 
242
- # 6. Calculate next version
243
- next_version = self.calculate_next_version(prod_version, increment_type)
248
+ # 6. Calculate next version
249
+ next_version = self.calculate_next_version(prod_version, increment_type)
244
250
 
245
- # 7. Verify stage file doesn't exist
246
- stage_file = self._releases_dir / f"{next_version}-stage.txt"
247
- if stage_file.exists():
248
- raise ReleaseFileError(
249
- f"Stage file already exists: {stage_file}\n"
250
- f"Version {next_version} is already in development.\n"
251
- f"To continue with this version, use existing stage file."
252
- )
251
+ # 7. Verify stage file doesn't exist
252
+ stage_file = self._releases_dir / f"{next_version}-stage.txt"
253
+ if stage_file.exists():
254
+ raise ReleaseFileError(
255
+ f"Stage file already exists: {stage_file}\n"
256
+ f"Version {next_version} is already in development.\n"
257
+ f"To continue with this version, use existing stage file."
258
+ )
253
259
 
254
- # 8. Create empty stage file
255
- stage_file.touch()
260
+ # 8. Create empty stage file
261
+ stage_file.touch()
256
262
 
257
- # 9. Commit
258
- self._repo.hgit.add(str(stage_file))
259
- self._repo.hgit.commit("-m", f"Prepare release {next_version}-stage")
263
+ # 9. Commit
264
+ self._repo.hgit.add(str(stage_file))
265
+ self._repo.hgit.commit("-m", f"Prepare release {next_version}-stage")
260
266
 
261
- # 10. Push to origin (global reservation)
262
- self._repo.hgit.push()
267
+ # 10. Push to origin (global reservation)
268
+ self._repo.hgit.push()
269
+ # Return result
270
+ return {
271
+ 'version': next_version,
272
+ 'file': str(stage_file),
273
+ 'previous_version': prod_version_str
274
+ }
275
+
276
+ finally:
277
+ # 11. ALWAYS release lock (even on error)
278
+ self._repo.hgit.release_branch_lock(lock_tag)
263
279
 
264
- # Return result
265
- return {
266
- 'version': next_version,
267
- 'file': str(stage_file),
268
- 'previous_version': prod_version_str
269
- }
270
280
 
271
281
  def _get_production_version(self) -> str:
272
282
  """
@@ -698,21 +708,24 @@ class ReleaseManager:
698
708
 
699
709
  Complete workflow with distributed lock to prevent race conditions:
700
710
  1. Pre-lock validations (branch, clean, patch exists)
701
- 2. Detect target stage file (auto or explicit)
702
- 3. Check patch not already in release
703
- 4. Acquire exclusive lock on ho-prod (atomic via Git tag)
704
- 5. Sync with origin (fetch + pull if needed)
705
- 6. Create temporary validation branch FROM ho-prod
706
- 7. Merge ALL patches already in release (from ho-release/X.Y.Z/* branches)
707
- 8. Merge new patch branch (from ho-patch/{patch_id})
708
- 9. Add patch to stage file on temp branch + commit
709
- 10. Run validation tests (with ALL patches integrated)
710
- 11. If tests fail: cleanup temp branch, release lock, exit with error
711
- 12. If tests pass: return to ho-prod, delete temp branch
712
- 13. Add patch to stage file on ho-prod + commit (file change only)
713
- 14. Push ho-prod to origin
714
- 16. Archive patch branch to ho-release/{version}/{patch_id}
715
- 17. Release lock (in finally block)
711
+ 2. Pre-lock check: detect stage file and verify patch not already in release (local state, fail-fast)
712
+ 3. **ACQUIRE LOCK on ho-prod** (prevents concurrent modifications)
713
+ 4. Fetch from origin (get latest state)
714
+ 5. Sync with origin/ho-prod (pull if behind)
715
+ 6. Re-detect target stage file (may have changed after sync)
716
+ 7. Re-check patch not already in release (may have changed after sync)
717
+ 8. Ensure patch branch synced with ho-prod
718
+ 9. Create temporary validation branch FROM ho-prod
719
+ 10. Merge ALL patches already in release (from ho-release/X.Y.Z/* branches)
720
+ 11. Merge new patch branch (from ho-patch/{patch_id})
721
+ 12. Add patch to stage file on temp branch + commit
722
+ 13. Run validation tests (with ALL patches integrated)
723
+ 14. If tests fail: cleanup temp branch, release lock, exit with error
724
+ 15. If tests pass: return to ho-prod, delete temp branch
725
+ 16. Add patch to stage file on ho-prod + commit (file change only)
726
+ 17. Push ho-prod to origin
727
+ 18. Archive patch branch to ho-release/{version}/{patch_id}
728
+ 19. **RELEASE LOCK** (in finally block, always executed)
716
729
 
717
730
  CRITICAL: ho-prod NEVER contains patch code directly. It only contains
718
731
  the releases/*.txt files that list which patches are in each release.
@@ -811,10 +824,8 @@ class ReleaseManager:
811
824
  f"Checkout branch first: git checkout ho-patch/{patch_id}"
812
825
  )
813
826
 
814
- # 2. Detect target stage file
827
+ # 2. Pre-lock validations on local state (fail-fast before acquiring lock)
815
828
  target_version, stage_file = self._detect_target_stage_file(to_version)
816
-
817
- # 3. Check patch not already in release
818
829
  existing_patches = self.read_release_patches(stage_file)
819
830
  if patch_id in existing_patches:
820
831
  raise ReleaseManagerError(
@@ -822,10 +833,39 @@ class ReleaseManager:
822
833
  f"Nothing to do."
823
834
  )
824
835
 
825
- # 4. ACQUIRE LOCK on ho-prod (with 30 min timeout for stale locks)
836
+ # 3. ACQUIRE LOCK on ho-prod (with 30 min timeout for stale locks)
826
837
  lock_tag = self._repo.hgit.acquire_branch_lock("ho-prod", timeout_minutes=30)
827
838
 
828
839
  try:
840
+ # 4. Fetch from origin to get latest state
841
+ self._repo.hgit.fetch_from_origin()
842
+
843
+ # 5. Sync with origin/ho-prod
844
+ is_synced, sync_status = self._repo.hgit.is_branch_synced("ho-prod")
845
+ if not is_synced:
846
+ if sync_status == "behind":
847
+ self._repo.hgit.pull()
848
+ elif sync_status == "diverged":
849
+ raise ReleaseManagerError(
850
+ "ho-prod has diverged from origin/ho-prod.\n"
851
+ "Manual resolution required:\n"
852
+ " git pull --rebase origin ho-prod\n"
853
+ " or\n"
854
+ " git merge origin/ho-prod"
855
+ )
856
+
857
+ # 6. Re-detect target stage file (may have changed after sync)
858
+ target_version, stage_file = self._detect_target_stage_file(to_version)
859
+
860
+ # 7. Re-check patch not already in release (may have changed after sync)
861
+ existing_patches = self.read_release_patches(stage_file)
862
+ if patch_id in existing_patches:
863
+ raise ReleaseManagerError(
864
+ f"Patch {patch_id} already in release {target_version}-stage.\n"
865
+ f"Nothing to do."
866
+ )
867
+
868
+ # 8. Ensure patch branch is synced with ho-prod
829
869
  sync_result = self._ensure_patch_branch_synced(patch_id)
830
870
 
831
871
  if sync_result['strategy'] != 'already-synced':
@@ -837,30 +877,12 @@ class ReleaseManager:
837
877
  file=sys.stderr
838
878
  )
839
879
 
840
- except ReleaseManagerError as e:
841
- # Manual resolution required - release lock and exit
842
- # Lock will be released in finally block
843
- raise
844
-
845
- temp_branch = f"temp-valid-{target_version}"
846
-
847
- try:
848
- # 5. Sync with origin (now that we have lock)
849
- self._repo.hgit.fetch_from_origin()
850
- is_synced, status = self._repo.hgit.is_branch_synced("ho-prod")
851
-
852
- if status == "behind":
853
- self._repo.hgit.pull()
854
- elif status == "diverged":
855
- raise ReleaseManagerError(
856
- "Branch ho-prod has diverged from origin.\n"
857
- "Manual merge or rebase required."
858
- )
880
+ temp_branch = f"temp-valid-{target_version}"
859
881
 
860
- # 6. Create temporary validation branch FROM ho-prod
882
+ # 9. Create temporary validation branch FROM ho-prod
861
883
  self._repo.hgit.checkout("-b", temp_branch)
862
884
 
863
- # 7. Merge ALL existing patches in the release (already validated)
885
+ # 10. Merge ALL existing patches in the release (already validated)
864
886
  for existing_patch_id in existing_patches:
865
887
  archived_branch = f"ho-release/{target_version}/{existing_patch_id}"
866
888
  if self._repo.hgit.branch_exists(archived_branch):
@@ -1397,24 +1419,13 @@ class ReleaseManager:
1397
1419
  "Commit or stash changes before promoting."
1398
1420
  )
1399
1421
 
1400
- # 2. Detect source and target (target-specific)
1401
- version, stage_file = self._detect_stage_to_promote()
1402
- if target != 'prod':
1403
- # RC: stage required, validate single active RC rule
1404
- self._validate_single_active_rc(version)
1405
- rc_number = self._determine_rc_number(version)
1406
- target_file = f"{version}-{target}{rc_number}.txt"
1407
- source_type = 'stage'
1408
- else: # target == 'prod'
1409
- # Production: stage optional, sequential version
1410
- stage_path = Path(self._releases_dir) / f"{version}-stage.txt"
1411
- if stage_path.exists():
1412
- stage_file = f"{version}-stage.txt"
1413
- source_type = 'stage'
1414
- else:
1415
- stage_file = None
1416
- source_type = 'empty'
1417
- target_file = f"{version}.txt"
1422
+ # 2. Pre-lock validation: check stage exists (preliminary check on local state)
1423
+ # This allows fast failure before acquiring lock
1424
+ try:
1425
+ version, stage_file = self._detect_stage_to_promote()
1426
+ except ReleaseManagerError:
1427
+ # No stage found locally - fail fast before lock
1428
+ raise
1418
1429
 
1419
1430
  # 3. Acquire distributed lock
1420
1431
  lock_tag = None
@@ -1434,6 +1445,25 @@ class ReleaseManager:
1434
1445
  "Resolve conflicts manually: git pull origin ho-prod"
1435
1446
  )
1436
1447
 
1448
+ # 5. Re-detect source and target (with up-to-date state after sync)
1449
+ version, stage_file = self._detect_stage_to_promote()
1450
+ if target != 'prod':
1451
+ # RC: stage required, validate single active RC rule
1452
+ self._validate_single_active_rc(version)
1453
+ rc_number = self._determine_rc_number(version)
1454
+ target_file = f"{version}-{target}{rc_number}.txt"
1455
+ source_type = 'stage'
1456
+ else: # target == 'prod'
1457
+ # Production: stage optional, sequential version
1458
+ stage_path = Path(self._releases_dir) / f"{version}-stage.txt"
1459
+ if stage_path.exists():
1460
+ stage_file = f"{version}-stage.txt"
1461
+ source_type = 'stage'
1462
+ else:
1463
+ stage_file = None
1464
+ source_type = 'empty'
1465
+ target_file = f"{version}.txt"
1466
+
1437
1467
  # 5. Apply patches to database (prod only)
1438
1468
  patches_applied = []
1439
1469
  if target == 'prod':
@@ -7,6 +7,7 @@ import os
7
7
  import sys
8
8
  from configparser import ConfigParser
9
9
  from pathlib import Path
10
+ import shutil
10
11
  import subprocess
11
12
  from typing import Optional
12
13
  from psycopg2 import OperationalError
@@ -595,6 +596,13 @@ class Repo:
595
596
  # Step 11: Initialize Git repository with ho-prod branch
596
597
  self._initialize_git_repository()
597
598
 
599
+ # step 12: Protect ho-prod from direct commits
600
+ hook_source = os.path.join(TEMPLATE_DIRS, 'pre-commit')
601
+ hook_dest = os.path.join(self.__base_dir, '.git', 'hooks', 'pre-commit')
602
+ shutil.copy(hook_source, hook_dest)
603
+ # Make hook executable
604
+ os.chmod(hook_dest, 0o755)
605
+
598
606
 
599
607
  def _validate_package_name(self, package_name):
600
608
  """
@@ -830,7 +838,7 @@ class Repo:
830
838
  # Creates .hop/config:
831
839
  # [halfORM]
832
840
  # package_name = my_blog
833
- # hop_version = 0.16.0
841
+ # hop_version = 0.17.0
834
842
  # devel = True
835
843
  # git_origin = https://github.com/user/my_blog.git
836
844
  """
@@ -7,9 +7,13 @@ conftest.py files in the appropriate subdirectories.
7
7
  Generated by half_orm v{hop_release}
8
8
  """
9
9
  import pytest
10
- from half_orm.model import Model
10
+ from {package_name} import MODEL
11
11
  from half_orm.relation import Relation
12
12
 
13
+ try:
14
+ from base_conftest import *
15
+ except ImportError:
16
+ pass
13
17
 
14
18
  @pytest.fixture(scope="session")
15
19
  def database_model():
@@ -24,7 +28,8 @@ def database_model():
24
28
  users = database_model.get_relation_class('public.users')
25
29
  assert users is not None
26
30
  """
27
- return Model("{package_name}")
31
+ MODEL.sql_trace = True
32
+ return MODEL
28
33
 
29
34
 
30
35
  @pytest.fixture
@@ -39,4 +44,4 @@ def relation_class():
39
44
  from {package_name}.public.users import Users
40
45
  assert issubclass(Users, relation_class)
41
46
  """
42
- return Relation
47
+ return Relation
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+
3
+ # Half-ORM pre-commit hook
4
+ # Protects ho-prod branch from direct commits
5
+ # Generated by half_orm_dev
6
+
7
+ # Get current branch
8
+ CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
9
+
10
+ # Check if we're on ho-prod branch
11
+ if [ "$CURRENT_BRANCH" != "ho-prod" ]; then
12
+ # Not on ho-prod, allow commit
13
+ exit 0
14
+ fi
15
+
16
+ # Check if this is a temporary validation tag commit (allows automated promotion)
17
+ CURRENT_TAG=$(git describe --exact-match --tags HEAD 2>/dev/null | grep -E '^temp-valid-')
18
+
19
+ if [ -n "$CURRENT_TAG" ]; then
20
+ # This is an automated promotion commit with temp tag, allow it
21
+ exit 0
22
+ fi
23
+
24
+ # Check if there's an active lock on ho-prod
25
+ # Lock tags follow pattern: lock-ho-prod-{timestamp}
26
+ LOCK_TAG=$(git tag -l 'lock-ho-prod-*' 2>/dev/null | head -n 1)
27
+
28
+ if [ -n "$LOCK_TAG" ]; then
29
+ # Lock is active, allow commit from Half-ORM workflow
30
+ exit 0
31
+ fi
32
+
33
+ # Direct commit on ho-prod is not allowed
34
+ cat << 'EOF'
35
+ ❌ Direct commits on 'ho-prod' are not allowed
36
+
37
+ The ho-prod branch is a protected production branch. Changes must be
38
+ promoted through the official Half-ORM release workflow.
39
+
40
+ To create a patch:
41
+ $ half_orm dev patch new <patch_id>
42
+
43
+ To add a patch to a release:
44
+ $ half_orm dev patch <patch_id> add-to-release [release]
45
+
46
+ To promote a stage release to rc/production:
47
+ $ half_orm dev release promote <rc|prod>
48
+
49
+ This workflow ensures:
50
+ • All patches are validated and tested
51
+ • Code is properly merged from patch branches
52
+ • Active patch branches are notified to rebase
53
+ • Production deploys are tracked and auditable
54
+
55
+ For more information, see the Half-ORM documentation on release management.
56
+
57
+ EOF
58
+
59
+ exit 1
@@ -0,0 +1 @@
1
+ 0.17.0-a1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.16.0a9
3
+ Version: 0.17.0a1
4
4
  Summary: half_orm development Framework.
5
5
  Home-page: https://github.com/collorg/halfORM_dev
6
6
  Author: Joël Maïzi
@@ -24,7 +24,7 @@ License-File: AUTHORS
24
24
  Requires-Dist: GitPython
25
25
  Requires-Dist: click
26
26
  Requires-Dist: pydash
27
- Requires-Dist: half_orm<0.17.0,>=0.16.0
27
+ Requires-Dist: half_orm<0.18.0,>=0.17.0
28
28
  Requires-Dist: pytest
29
29
  Dynamic: author
30
30
  Dynamic: author-email
@@ -927,7 +927,7 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
927
927
 
928
928
  ---
929
929
 
930
- **Version**: 0.16.0
930
+ **Version**: 0.17.0
931
931
  **halfORM**: Compatible with halfORM 0.16.x
932
932
  **Python**: 3.8+
933
933
  **PostgreSQL**: Tested with 13+ (might work with earlier versions)
@@ -53,6 +53,7 @@ half_orm_dev/templates/init_module_template
53
53
  half_orm_dev/templates/module_template_1
54
54
  half_orm_dev/templates/module_template_2
55
55
  half_orm_dev/templates/module_template_3
56
+ half_orm_dev/templates/pre-commit
56
57
  half_orm_dev/templates/relation_test
57
58
  half_orm_dev/templates/setup.py
58
59
  half_orm_dev/templates/sql_adapter
@@ -1,5 +1,5 @@
1
1
  GitPython
2
2
  click
3
3
  pydash
4
- half_orm<0.17.0,>=0.16.0
4
+ half_orm<0.18.0,>=0.17.0
5
5
  pytest
@@ -1 +0,0 @@
1
- 0.16.0-a9
File without changes
File without changes