half-orm-dev 0.17.3a5__py3-none-any.whl → 0.17.3a7__py3-none-any.whl
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.
- half_orm_dev/cli/commands/release.py +103 -24
- half_orm_dev/database.py +66 -5
- half_orm_dev/patch_manager.py +1 -1
- half_orm_dev/release_manager.py +350 -55
- half_orm_dev/repo.py +94 -25
- half_orm_dev/scripts/repair-metadata.py +352 -0
- half_orm_dev/templates/README +36 -2
- half_orm_dev/version.txt +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/METADATA +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/RECORD +14 -13
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/WHEEL +1 -1
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/licenses/AUTHORS +0 -0
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/licenses/LICENSE +0 -0
- {half_orm_dev-0.17.3a5.dist-info → half_orm_dev-0.17.3a7.dist-info}/top_level.txt +0 -0
half_orm_dev/release_manager.py
CHANGED
|
@@ -744,27 +744,64 @@ class ReleaseManager:
|
|
|
744
744
|
for patch_id in stage_patches:
|
|
745
745
|
self._repo.patch_manager.apply_patch_files(patch_id, self._repo.model)
|
|
746
746
|
|
|
747
|
-
def
|
|
747
|
+
def _collect_all_version_patches(self, version: str) -> List[str]:
|
|
748
748
|
"""
|
|
749
|
-
|
|
749
|
+
Collect all patches for a version including hotfixes.
|
|
750
|
+
|
|
751
|
+
Returns patches from:
|
|
752
|
+
1. Base release (X.Y.Z.txt)
|
|
753
|
+
2. All hotfixes in order (X.Y.Z-hotfix1.txt, X.Y.Z-hotfix2.txt, ...)
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
version: Base version string (e.g., "1.2.0")
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
List of all patch IDs in application order
|
|
760
|
+
|
|
761
|
+
Examples:
|
|
762
|
+
# With 1.2.0.txt containing [a, b] and 1.2.0-hotfix1.txt containing [c]
|
|
763
|
+
patches = mgr._collect_all_version_patches("1.2.0")
|
|
764
|
+
# Returns: ["a", "b", "c"]
|
|
765
|
+
"""
|
|
766
|
+
all_patches = []
|
|
767
|
+
|
|
768
|
+
# 1. Base release patches
|
|
769
|
+
base_file = f"{version}.txt"
|
|
770
|
+
all_patches.extend(self.read_release_patches(base_file))
|
|
771
|
+
|
|
772
|
+
# 2. Hotfix patches in order
|
|
773
|
+
hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
|
|
774
|
+
for hotfix_file in hotfix_files:
|
|
775
|
+
all_patches.extend(self.read_release_patches(hotfix_file.name))
|
|
776
|
+
|
|
777
|
+
return all_patches
|
|
778
|
+
|
|
779
|
+
def _generate_data_sql_file(self, patch_list: List[str], version: str) -> Optional[Path]:
|
|
780
|
+
"""
|
|
781
|
+
Generate model/data-X.Y.Z.sql file from patches with @HOP:data annotation.
|
|
750
782
|
|
|
751
783
|
Collects all SQL files marked with `-- @HOP:data` from the patch list
|
|
752
784
|
and concatenates them into a single data SQL file for from-scratch
|
|
753
|
-
installations.
|
|
785
|
+
installations (clone, restore_database_from_schema).
|
|
786
|
+
|
|
787
|
+
This file is only generated for production releases. RC and hotfix
|
|
788
|
+
versions don't need this file because:
|
|
789
|
+
- In production upgrades, data is inserted by patch application
|
|
790
|
+
- This file is only for from-scratch installations
|
|
754
791
|
|
|
755
792
|
Args:
|
|
756
793
|
patch_list: List of patch IDs to process
|
|
757
|
-
|
|
794
|
+
version: Version string (e.g., "0.17.0")
|
|
758
795
|
|
|
759
796
|
Returns:
|
|
760
|
-
Path to generated file, or None if no data files found
|
|
797
|
+
Path to generated file (model/data-X.Y.Z.sql), or None if no data files found
|
|
761
798
|
|
|
762
799
|
Examples:
|
|
763
800
|
self._generate_data_sql_file(
|
|
764
801
|
["456-auth", "457-roles"],
|
|
765
|
-
"
|
|
802
|
+
"0.17.0"
|
|
766
803
|
)
|
|
767
|
-
# Generates
|
|
804
|
+
# Generates model/data-0.17.0.sql with data from both patches
|
|
768
805
|
"""
|
|
769
806
|
if not patch_list:
|
|
770
807
|
return None
|
|
@@ -777,16 +814,17 @@ class ReleaseManager:
|
|
|
777
814
|
# No data files found - skip generation
|
|
778
815
|
return None
|
|
779
816
|
|
|
780
|
-
# Generate output file
|
|
781
|
-
|
|
817
|
+
# Generate output file in model/ directory
|
|
818
|
+
output_filename = f"data-{version}.sql"
|
|
819
|
+
output_path = Path(self._repo.model_dir) / output_filename
|
|
782
820
|
|
|
783
821
|
with output_path.open('w', encoding='utf-8') as out_file:
|
|
784
822
|
# Write header
|
|
785
|
-
out_file.write(f"-- Data file for {
|
|
823
|
+
out_file.write(f"-- Data file for version {version}\n")
|
|
786
824
|
out_file.write(f"-- Generated from patches: {', '.join(patch_list)}\n")
|
|
787
825
|
out_file.write(f"-- This file contains reference data (DML) for from-scratch installations\n")
|
|
788
826
|
out_file.write(f"--\n")
|
|
789
|
-
out_file.write(f"-- Usage:
|
|
827
|
+
out_file.write(f"-- Usage: Automatically loaded by restore_database_from_schema()\n")
|
|
790
828
|
out_file.write(f"--\n\n")
|
|
791
829
|
|
|
792
830
|
# Concatenate all data files
|
|
@@ -811,26 +849,30 @@ class ReleaseManager:
|
|
|
811
849
|
|
|
812
850
|
except Exception as e:
|
|
813
851
|
raise ReleaseManagerError(
|
|
814
|
-
f"Failed to generate data SQL file {
|
|
852
|
+
f"Failed to generate data SQL file data-{version}.sql: {e}"
|
|
815
853
|
)
|
|
816
854
|
|
|
817
855
|
def get_all_release_context_patches(self) -> List[str]:
|
|
818
856
|
"""
|
|
819
|
-
|
|
857
|
+
Get all validated patches for the next release context.
|
|
858
|
+
|
|
859
|
+
Sequential application of incremental RCs + staged patches from TOML.
|
|
860
|
+
- rc1: initial patches (e.g., 123, 456, 789)
|
|
861
|
+
- rc2: new patches (e.g., 999)
|
|
862
|
+
- rc3: new patches (e.g., 888, 777)
|
|
863
|
+
- TOML: only "staged" patches (validated via patch merge)
|
|
820
864
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
- rc3: patches nouveaux (ex: 888, 777)
|
|
825
|
-
- TOML: TOUS les patches (candidates + staged) dans l'ordre
|
|
865
|
+
"candidate" patches are NOT included because they have not passed
|
|
866
|
+
the validation process (tests) that occurs during patch merge.
|
|
867
|
+
Only "staged" patches are guaranteed to have passed tests.
|
|
826
868
|
|
|
827
|
-
|
|
869
|
+
The current patch is applied separately by apply_patch_complete_workflow.
|
|
828
870
|
|
|
829
|
-
|
|
871
|
+
Note: A future "release apply" command could allow applying all patches
|
|
872
|
+
(candidates + staged) in a temporary branch for integration testing.
|
|
830
873
|
|
|
831
874
|
Returns:
|
|
832
|
-
|
|
833
|
-
Inclut RC files + TOUS les patches du TOML (candidates + staged)
|
|
875
|
+
Ordered list of validated patch IDs (RC + staged)
|
|
834
876
|
|
|
835
877
|
Examples:
|
|
836
878
|
# Production: 1.3.5
|
|
@@ -839,14 +881,15 @@ class ReleaseManager:
|
|
|
839
881
|
# 1.3.6-patches.toml: {"234": "candidate", "567": "staged"}
|
|
840
882
|
|
|
841
883
|
patches = mgr.get_all_release_context_patches()
|
|
842
|
-
# → ["123", "456", "789", "999", "
|
|
884
|
+
# → ["123", "456", "789", "999", "567"]
|
|
885
|
+
# Note: "234" (candidate) is not included - not yet validated
|
|
843
886
|
|
|
844
|
-
#
|
|
887
|
+
# For apply-patch on patch 234:
|
|
845
888
|
# 1. Restore DB (1.3.5)
|
|
846
889
|
# 2. Apply 123, 456, 789 (rc1)
|
|
847
890
|
# 3. Apply 999 (rc2)
|
|
848
|
-
# 4. Apply
|
|
849
|
-
# 5. Apply
|
|
891
|
+
# 4. Apply 567 (staged from TOML)
|
|
892
|
+
# 5. Apply 234 (current patch, applied separately)
|
|
850
893
|
"""
|
|
851
894
|
next_version = self.get_next_release_version()
|
|
852
895
|
|
|
@@ -855,20 +898,21 @@ class ReleaseManager:
|
|
|
855
898
|
|
|
856
899
|
all_patches = []
|
|
857
900
|
|
|
858
|
-
# 1.
|
|
901
|
+
# 1. Apply all RCs in order (incremental)
|
|
859
902
|
rc_files = self._get_label_files(next_version, 'rc')
|
|
860
903
|
for rc_file in rc_files:
|
|
861
904
|
patches = self.read_release_patches(rc_file)
|
|
862
|
-
#
|
|
905
|
+
# Each RC is incremental, no deduplication needed
|
|
863
906
|
all_patches.extend(patches)
|
|
864
907
|
|
|
865
|
-
# 2.
|
|
866
|
-
#
|
|
908
|
+
# 2. Apply only "staged" patches from TOML
|
|
909
|
+
# "candidate" patches are excluded because they have not yet passed
|
|
910
|
+
# the validation process (tests) that occurs during patch merge
|
|
911
|
+
# Current patch is applied separately by apply_patch_complete_workflow
|
|
867
912
|
release_file = ReleaseFile(next_version, self._releases_dir)
|
|
868
913
|
if release_file.exists():
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
all_patches.extend(all_toml_patches)
|
|
914
|
+
staged_patches = release_file.get_patches(status="staged")
|
|
915
|
+
all_patches.extend(staged_patches)
|
|
872
916
|
|
|
873
917
|
return all_patches
|
|
874
918
|
|
|
@@ -2291,10 +2335,18 @@ class ReleaseManager:
|
|
|
2291
2335
|
# 1. Apply patches to database (for validation)
|
|
2292
2336
|
self._apply_release_patches(version)
|
|
2293
2337
|
|
|
2294
|
-
# 2.
|
|
2338
|
+
# 2. Register the RC version in half_orm_meta.hop_release
|
|
2339
|
+
version_parts = version.split('.')
|
|
2340
|
+
major, minor, patch_num = map(int, version_parts)
|
|
2341
|
+
self._repo.database.register_release(
|
|
2342
|
+
major, minor, patch_num,
|
|
2343
|
+
pre_release='rc', pre_release_num=str(rc_number)
|
|
2344
|
+
)
|
|
2345
|
+
|
|
2346
|
+
# 3. Checkout release branch
|
|
2295
2347
|
self._repo.hgit.checkout(release_branch)
|
|
2296
2348
|
|
|
2297
|
-
#
|
|
2349
|
+
# 4. Create RC tag on release branch
|
|
2298
2350
|
self._repo.hgit.create_tag(rc_tag, f"Release Candidate %{version}")
|
|
2299
2351
|
|
|
2300
2352
|
# Push tag
|
|
@@ -2310,14 +2362,10 @@ class ReleaseManager:
|
|
|
2310
2362
|
|
|
2311
2363
|
# Keep TOML file for continued development (don't delete it)
|
|
2312
2364
|
|
|
2313
|
-
#
|
|
2314
|
-
|
|
2315
|
-
data_file = self._generate_data_sql_file(
|
|
2316
|
-
rc_patches,
|
|
2317
|
-
f"data-{version}-rc{rc_number}.sql"
|
|
2318
|
-
)
|
|
2365
|
+
# Note: data-X.Y.Z.sql is only generated for production releases
|
|
2366
|
+
# RC releases don't need it - data is inserted via patch application
|
|
2319
2367
|
|
|
2320
|
-
# Commit RC snapshot
|
|
2368
|
+
# Commit RC snapshot (in .hop/releases/)
|
|
2321
2369
|
# This also syncs .hop/ to all active branches automatically
|
|
2322
2370
|
self._repo.commit_and_sync_to_active_branches(
|
|
2323
2371
|
message=f"[HOP] Promote release %{version} to RC {rc_number}"
|
|
@@ -2474,6 +2522,11 @@ class ReleaseManager:
|
|
|
2474
2522
|
# The database should already be in the correct state from RC
|
|
2475
2523
|
pass
|
|
2476
2524
|
|
|
2525
|
+
# Register the release version in half_orm_meta.hop_release
|
|
2526
|
+
version_parts = version.split('.')
|
|
2527
|
+
major, minor, patch_num = map(int, version_parts)
|
|
2528
|
+
self._repo.database.register_release(major, minor, patch_num)
|
|
2529
|
+
|
|
2477
2530
|
# Generate schema dump for this production version
|
|
2478
2531
|
model_dir = Path(self._repo.model_dir)
|
|
2479
2532
|
self._repo.database._generate_schema_sql(version, model_dir)
|
|
@@ -2489,13 +2542,12 @@ class ReleaseManager:
|
|
|
2489
2542
|
if toml_file.exists():
|
|
2490
2543
|
toml_file.unlink()
|
|
2491
2544
|
|
|
2492
|
-
# Generate data-X.Y.Z.sql if any patches have @HOP:data files
|
|
2493
|
-
# This
|
|
2545
|
+
# Generate model/data-X.Y.Z.sql if any patches have @HOP:data files
|
|
2546
|
+
# This file is used for from-scratch installations (clone, restore)
|
|
2494
2547
|
prod_patches = self.read_release_patches(prod_file.name)
|
|
2495
|
-
self._generate_data_sql_file(
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
)
|
|
2548
|
+
data_file = self._generate_data_sql_file(prod_patches, version)
|
|
2549
|
+
if data_file:
|
|
2550
|
+
self._repo.hgit.add(str(data_file))
|
|
2499
2551
|
|
|
2500
2552
|
self._repo.commit_and_sync_to_active_branches(
|
|
2501
2553
|
message=f"[HOP] Promote release %{version} to production",
|
|
@@ -2910,14 +2962,12 @@ class ReleaseManager:
|
|
|
2910
2962
|
if toml_file.exists():
|
|
2911
2963
|
self._repo.hgit.rm(str(toml_file))
|
|
2912
2964
|
|
|
2913
|
-
#
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
f"data-{version}-hotfix{hotfix_num}.sql"
|
|
2918
|
-
)
|
|
2965
|
+
# Regenerate model/data-X.Y.Z.sql with all patches (original release + all hotfixes)
|
|
2966
|
+
# This ensures from-scratch installations get all data
|
|
2967
|
+
all_patches = self._collect_all_version_patches(version)
|
|
2968
|
+
data_file = self._generate_data_sql_file(all_patches, version)
|
|
2919
2969
|
if data_file:
|
|
2920
|
-
self._repo.hgit.add(str(data_file))
|
|
2970
|
+
self._repo.hgit.add(str(data_file))
|
|
2921
2971
|
|
|
2922
2972
|
# 6. Apply release patches and generate SQL dumps
|
|
2923
2973
|
self._apply_release_patches(version, True)
|
|
@@ -2954,3 +3004,248 @@ class ReleaseManager:
|
|
|
2954
3004
|
raise
|
|
2955
3005
|
except Exception as e:
|
|
2956
3006
|
raise ReleaseManagerError(f"Failed to promote hotfix: {e}")
|
|
3007
|
+
|
|
3008
|
+
def get_all_release_patches_for_testing(self) -> List[str]:
|
|
3009
|
+
"""
|
|
3010
|
+
Get ALL patches for integration testing (candidates + staged).
|
|
3011
|
+
|
|
3012
|
+
Unlike get_all_release_context_patches() which excludes candidates,
|
|
3013
|
+
this method returns ALL patches including those not yet validated.
|
|
3014
|
+
Used by 'release apply' for complete integration testing.
|
|
3015
|
+
|
|
3016
|
+
Returns:
|
|
3017
|
+
Ordered list of ALL patch IDs (RC + candidates + staged)
|
|
3018
|
+
|
|
3019
|
+
Examples:
|
|
3020
|
+
# Production: 1.3.5
|
|
3021
|
+
# 1.3.6-rc1.txt: 123, 456, 789
|
|
3022
|
+
# 1.3.6-patches.toml: {"234": "candidate", "567": "staged"}
|
|
3023
|
+
|
|
3024
|
+
patches = mgr.get_all_release_patches_for_testing()
|
|
3025
|
+
# → ["123", "456", "789", "234", "567"]
|
|
3026
|
+
# All patches included for complete integration testing
|
|
3027
|
+
"""
|
|
3028
|
+
next_version = self.get_next_release_version()
|
|
3029
|
+
|
|
3030
|
+
if not next_version:
|
|
3031
|
+
return []
|
|
3032
|
+
|
|
3033
|
+
all_patches = []
|
|
3034
|
+
|
|
3035
|
+
# 1. Apply all RCs in order (incremental)
|
|
3036
|
+
rc_files = self._get_label_files(next_version, 'rc')
|
|
3037
|
+
for rc_file in rc_files:
|
|
3038
|
+
patches = self.read_release_patches(rc_file)
|
|
3039
|
+
all_patches.extend(patches)
|
|
3040
|
+
|
|
3041
|
+
# 2. Apply ALL patches from TOML (candidates + staged)
|
|
3042
|
+
# For integration testing, we want to test the complete release
|
|
3043
|
+
release_file = ReleaseFile(next_version, self._releases_dir)
|
|
3044
|
+
if release_file.exists():
|
|
3045
|
+
all_toml_patches = release_file.get_patches() # No status filter = all
|
|
3046
|
+
all_patches.extend(all_toml_patches)
|
|
3047
|
+
|
|
3048
|
+
return all_patches
|
|
3049
|
+
|
|
3050
|
+
def _cleanup_validate_branch(self, original_branch: Optional[str],
|
|
3051
|
+
validate_branch: Optional[str]) -> None:
|
|
3052
|
+
"""
|
|
3053
|
+
Cleanup temporary validation branch after apply_release.
|
|
3054
|
+
|
|
3055
|
+
Switches back to the original branch and deletes the temporary
|
|
3056
|
+
validation branch. Errors are silently ignored for robustness.
|
|
3057
|
+
|
|
3058
|
+
Args:
|
|
3059
|
+
original_branch: Branch to switch back to (may be None)
|
|
3060
|
+
validate_branch: Temporary branch to delete (may be None)
|
|
3061
|
+
"""
|
|
3062
|
+
try:
|
|
3063
|
+
if original_branch:
|
|
3064
|
+
self._repo.hgit.checkout(original_branch)
|
|
3065
|
+
except Exception:
|
|
3066
|
+
pass # Best effort
|
|
3067
|
+
|
|
3068
|
+
try:
|
|
3069
|
+
if validate_branch:
|
|
3070
|
+
self._repo.hgit.delete_branch(validate_branch, force=True)
|
|
3071
|
+
except Exception:
|
|
3072
|
+
pass # Best effort
|
|
3073
|
+
|
|
3074
|
+
def apply_release(self, run_tests: bool = True) -> dict:
|
|
3075
|
+
"""
|
|
3076
|
+
Apply all patches from current release for integration testing.
|
|
3077
|
+
|
|
3078
|
+
Creates a temporary validation branch (ho-validate/release-X.Y.Z),
|
|
3079
|
+
merges candidate patch branches, restores the database, applies ALL
|
|
3080
|
+
patches (including candidates), optionally runs tests, then cleans up.
|
|
3081
|
+
This simulates a complete release merge without modifying the release branch.
|
|
3082
|
+
|
|
3083
|
+
Unlike 'patch apply' which only applies staged patches,
|
|
3084
|
+
'release apply' applies ALL patches (candidates + staged) to
|
|
3085
|
+
validate the complete integration.
|
|
3086
|
+
|
|
3087
|
+
Args:
|
|
3088
|
+
run_tests: Whether to run pytest after applying patches (default: True)
|
|
3089
|
+
|
|
3090
|
+
Returns:
|
|
3091
|
+
Dict containing:
|
|
3092
|
+
- version: Release version being tested
|
|
3093
|
+
- patches_applied: List of patch IDs applied
|
|
3094
|
+
- candidates_merged: List of candidate patch branches merged
|
|
3095
|
+
- files_applied: List of SQL/Python files applied
|
|
3096
|
+
- tests_passed: Boolean (None if tests not run)
|
|
3097
|
+
- test_output: Test output (None if tests not run)
|
|
3098
|
+
- status: 'success' or 'failed'
|
|
3099
|
+
- error: Error message if failed
|
|
3100
|
+
|
|
3101
|
+
Raises:
|
|
3102
|
+
ReleaseManagerError: If no release in development or apply fails
|
|
3103
|
+
|
|
3104
|
+
Workflow:
|
|
3105
|
+
1. Detect current development release
|
|
3106
|
+
2. Validate we're on release branch
|
|
3107
|
+
3. Create temporary validation branch
|
|
3108
|
+
4. Merge candidate patch branches (simulate future merges)
|
|
3109
|
+
5. Restore database from production schema
|
|
3110
|
+
6. Apply ALL patches (RC + staged + candidates)
|
|
3111
|
+
7. Generate Python code
|
|
3112
|
+
8. Optionally run tests
|
|
3113
|
+
9. Cleanup: switch back and delete temp branch
|
|
3114
|
+
10. Return results
|
|
3115
|
+
|
|
3116
|
+
Examples:
|
|
3117
|
+
# Test current release with tests
|
|
3118
|
+
result = release_mgr.apply_release()
|
|
3119
|
+
if result['status'] == 'success':
|
|
3120
|
+
print(f"Release {result['version']} ready!")
|
|
3121
|
+
|
|
3122
|
+
# Test without running tests
|
|
3123
|
+
result = release_mgr.apply_release(run_tests=False)
|
|
3124
|
+
"""
|
|
3125
|
+
import subprocess
|
|
3126
|
+
|
|
3127
|
+
validate_branch = None
|
|
3128
|
+
original_branch = None
|
|
3129
|
+
|
|
3130
|
+
try:
|
|
3131
|
+
# 1. Detect current development release
|
|
3132
|
+
next_version = self.get_next_release_version()
|
|
3133
|
+
if not next_version:
|
|
3134
|
+
raise ReleaseManagerError(
|
|
3135
|
+
"No development release found.\n"
|
|
3136
|
+
"Create one with: half_orm dev release create <level>"
|
|
3137
|
+
)
|
|
3138
|
+
|
|
3139
|
+
# 2. Validate we're on a release branch
|
|
3140
|
+
original_branch = self._repo.hgit.branch
|
|
3141
|
+
expected_branch = f"ho-release/{next_version}"
|
|
3142
|
+
if original_branch != expected_branch:
|
|
3143
|
+
raise ReleaseManagerError(
|
|
3144
|
+
f"Must be on release branch {expected_branch}\n"
|
|
3145
|
+
f"Currently on: {original_branch}\n"
|
|
3146
|
+
f"Switch with: git checkout {expected_branch}"
|
|
3147
|
+
)
|
|
3148
|
+
|
|
3149
|
+
# 3. Create temporary validation branch
|
|
3150
|
+
validate_branch = f"ho-validate/release-{next_version}"
|
|
3151
|
+
|
|
3152
|
+
# Delete existing validation branch if it exists
|
|
3153
|
+
try:
|
|
3154
|
+
self._repo.hgit.delete_branch(validate_branch, force=True)
|
|
3155
|
+
except Exception:
|
|
3156
|
+
pass # Branch doesn't exist, that's fine
|
|
3157
|
+
|
|
3158
|
+
# Create and checkout validation branch
|
|
3159
|
+
self._repo.hgit.create_branch(validate_branch)
|
|
3160
|
+
self._repo.hgit.checkout(validate_branch)
|
|
3161
|
+
|
|
3162
|
+
# 4. Merge candidate patch branches to simulate future merges
|
|
3163
|
+
# Staged patches are already merged on ho-release, only candidates need merging
|
|
3164
|
+
release_file = ReleaseFile(next_version, self._releases_dir)
|
|
3165
|
+
candidates_merged = []
|
|
3166
|
+
if release_file.exists():
|
|
3167
|
+
candidate_patches = release_file.get_patches(status="candidate")
|
|
3168
|
+
for patch_id in candidate_patches:
|
|
3169
|
+
patch_branch = f"ho-patch/{patch_id}"
|
|
3170
|
+
try:
|
|
3171
|
+
self._repo.hgit.merge(patch_branch)
|
|
3172
|
+
candidates_merged.append(patch_id)
|
|
3173
|
+
except Exception as e:
|
|
3174
|
+
raise ReleaseManagerError(
|
|
3175
|
+
f"Failed to merge candidate branch {patch_branch}: {e}\n"
|
|
3176
|
+
f"Fix merge conflicts on the patch branch first."
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3179
|
+
# 5. Restore database from production schema
|
|
3180
|
+
self._repo.restore_database_from_schema()
|
|
3181
|
+
|
|
3182
|
+
# 6. Get and apply ALL patches (RC + staged + candidates)
|
|
3183
|
+
all_patches = self.get_all_release_patches_for_testing()
|
|
3184
|
+
all_applied_files = []
|
|
3185
|
+
|
|
3186
|
+
for patch_id in all_patches:
|
|
3187
|
+
files = self._repo.patch_manager.apply_patch_files(
|
|
3188
|
+
patch_id, self._repo.model
|
|
3189
|
+
)
|
|
3190
|
+
all_applied_files.extend(files)
|
|
3191
|
+
|
|
3192
|
+
# 7. Generate Python code
|
|
3193
|
+
from half_orm_dev import modules
|
|
3194
|
+
modules.generate(self._repo)
|
|
3195
|
+
|
|
3196
|
+
# 8. Optionally run tests
|
|
3197
|
+
tests_passed = None
|
|
3198
|
+
test_output = None
|
|
3199
|
+
|
|
3200
|
+
if run_tests:
|
|
3201
|
+
try:
|
|
3202
|
+
result = subprocess.run(
|
|
3203
|
+
['pytest', '-v'],
|
|
3204
|
+
cwd=self._repo.base_dir,
|
|
3205
|
+
capture_output=True,
|
|
3206
|
+
text=True,
|
|
3207
|
+
timeout=600 # 10 minute timeout
|
|
3208
|
+
)
|
|
3209
|
+
tests_passed = result.returncode == 0
|
|
3210
|
+
test_output = result.stdout + result.stderr
|
|
3211
|
+
except subprocess.TimeoutExpired:
|
|
3212
|
+
tests_passed = False
|
|
3213
|
+
test_output = "Tests timed out after 10 minutes"
|
|
3214
|
+
except FileNotFoundError:
|
|
3215
|
+
tests_passed = None
|
|
3216
|
+
test_output = "pytest not found - tests skipped"
|
|
3217
|
+
|
|
3218
|
+
# 9. Cleanup: switch back to original branch and delete temp branch
|
|
3219
|
+
self._repo.hgit.checkout(original_branch)
|
|
3220
|
+
try:
|
|
3221
|
+
self._repo.hgit.delete_branch(validate_branch, force=True)
|
|
3222
|
+
except Exception:
|
|
3223
|
+
pass # Best effort cleanup
|
|
3224
|
+
|
|
3225
|
+
# 10. Return results
|
|
3226
|
+
return {
|
|
3227
|
+
'version': next_version,
|
|
3228
|
+
'patches_applied': all_patches,
|
|
3229
|
+
'candidates_merged': candidates_merged,
|
|
3230
|
+
'files_applied': all_applied_files,
|
|
3231
|
+
'tests_passed': tests_passed,
|
|
3232
|
+
'test_output': test_output,
|
|
3233
|
+
'status': 'success' if tests_passed is not False else 'failed',
|
|
3234
|
+
'error': None
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
except ReleaseManagerError:
|
|
3238
|
+
# Cleanup on error
|
|
3239
|
+
self._cleanup_validate_branch(original_branch, validate_branch)
|
|
3240
|
+
raise
|
|
3241
|
+
except Exception as e:
|
|
3242
|
+
# Cleanup on error
|
|
3243
|
+
self._cleanup_validate_branch(original_branch, validate_branch)
|
|
3244
|
+
|
|
3245
|
+
# Restore DB to clean state on failure
|
|
3246
|
+
try:
|
|
3247
|
+
self._repo.restore_database_from_schema()
|
|
3248
|
+
except Exception:
|
|
3249
|
+
pass # Best effort cleanup
|
|
3250
|
+
|
|
3251
|
+
raise ReleaseManagerError(f"Release apply failed: {e}")
|