@@ -524,6 +524,122 @@ def _promote_full_release_from_staging(
524524 )
525525
526526
527+ def _promotion_result_from_stdout (promotion_stdout : str ):
528+ """Parse typed promotion results from the promotion subprocess output."""
529+
530+ from policyengine_us_data .release_promotion import FullPromotionResult
531+
532+ try :
533+ payload = json .loads (promotion_stdout )
534+ except json .JSONDecodeError as exc :
535+ raise RuntimeError (
536+ "Full release promotion subprocess did not return JSON."
537+ ) from exc
538+ return FullPromotionResult .from_legacy_dict (payload )
539+
540+
541+ def _release_promotion_context_from_run_context (run_context : RunContext ):
542+ """Build the Stage 5 library context from the orchestration run context."""
543+
544+ from policyengine_us_data .release_promotion import ReleasePromotionContext
545+
546+ return ReleasePromotionContext (
547+ run_id = run_context .run_id ,
548+ candidate_version = run_context .candidate_version ,
549+ release_version = run_context .release_version ,
550+ hf_repo_name = "policyengine/policyengine-us-data" ,
551+ gcs_bucket_name = "policyengine-us-data" ,
552+ base_release_version = run_context .base_release_version or None ,
553+ release_bump = run_context .release_bump or None ,
554+ modal_app_name = run_context .modal_app_name or None ,
555+ modal_environment = run_context .modal_environment or None ,
556+ hf_staging_prefix = run_context .hf_staging_prefix or None ,
557+ metadata = {"run_context" : run_context .to_dict ()},
558+ )
559+
560+
561+ def _release_artifact_metadata_by_path (
562+ run_id : str ,
563+ rel_paths : list [str ],
564+ ) -> dict [str , dict [str , object ]]:
565+ """Return local checksum/size metadata for staged release artifacts."""
566+
567+ metadata : dict [str , dict [str , object ]] = {}
568+ for local_path , rel_path in _full_release_manifest_files (run_id , rel_paths ):
569+ path = Path (local_path )
570+ if not path .exists () or not path .is_file ():
571+ continue
572+ reference = ArtifactReference .from_path (path )
573+ metadata [rel_path ] = {
574+ "sha256" : f"sha256:{ reference .sha256 } " ,
575+ "size_bytes" : reference .size_bytes ,
576+ }
577+ return metadata
578+
579+
580+ def _stage4_output_contract_repo_path_if_available (run_id : str ) -> str | None :
581+ """Return the run-repo path for the Stage 4 contract when it exists locally."""
582+
583+ run_dir = _run_dir (run_id )
584+ candidates = (
585+ run_dir / "diagnostics" / "contracts" / "output_build_contract.json" ,
586+ run_dir / "contracts" / "output_build_contract.json" ,
587+ run_dir / "output_build_contract.json" ,
588+ )
589+ for path in candidates :
590+ if path .exists () and path .is_file ():
591+ return f"calibration/runs/{ run_id } /{ path .relative_to (run_dir ).as_posix ()} "
592+ return None
593+
594+
595+ def _write_release_promotion_contract_for_run (
596+ * ,
597+ meta : RunMetadata ,
598+ run_context : RunContext ,
599+ rel_paths : list [str ],
600+ promotion_result ,
601+ ) -> ArtifactReference :
602+ """Write Stage 5's run-local contract and return its manifest reference."""
603+
604+ from policyengine_us_data .release_promotion import (
605+ build_legacy_release_candidate_bundle ,
606+ release_promotion_contract_path ,
607+ write_release_promotion_contract ,
608+ )
609+
610+ run_dir = _run_dir (run_context .run_id )
611+ contract_path = release_promotion_contract_path (run_dir )
612+ candidate_bundle = build_legacy_release_candidate_bundle (
613+ context = _release_promotion_context_from_run_context (run_context ),
614+ rel_paths = rel_paths ,
615+ artifact_metadata_by_path = _release_artifact_metadata_by_path (
616+ run_context .run_id ,
617+ rel_paths ,
618+ ),
619+ source_output_contract_path = _stage4_output_contract_repo_path_if_available (
620+ run_context .run_id
621+ ),
622+ )
623+ write_release_promotion_contract (
624+ contract_path = contract_path ,
625+ candidate_bundle = candidate_bundle ,
626+ promotion_result = promotion_result ,
627+ created_at = datetime .now (timezone .utc ).isoformat (),
628+ code_sha = meta .sha ,
629+ package_version = meta .version ,
630+ metadata = {
631+ "writer" : "modal_app.pipeline.promote_run" ,
632+ "branch" : meta .branch ,
633+ },
634+ )
635+ return ArtifactReference .from_path (
636+ contract_path ,
637+ role = "contract" ,
638+ base_dir = run_dir ,
639+ media_type = "application/json" ,
640+ )
641+
642+
527643@app .function (
528644 image = image ,
529645 timeout = 300 ,
@@ -2013,6 +2129,13 @@ def promote_run(
20132129 promotion_context .to_dict (),
20142130 )
20152131 print (f" { promotion_stdout } " )
2132+ promotion_result = _promotion_result_from_stdout (promotion_stdout )
2133+ release_promotion_contract_ref = _write_release_promotion_contract_for_run (
2134+ meta = meta ,
2135+ run_context = promotion_context ,
2136+ rel_paths = rel_paths ,
2137+ promotion_result = promotion_result ,
2138+ )
20162139
20172140 # Update run status only after all required promotion work succeeds.
20182141 meta .status = "promoted"
@@ -2021,8 +2144,11 @@ def promote_run(
20212144 _complete_step_manifest (
20222145 promote_manifest ,
20232146 outputs = [
2024- ArtifactReference .from_dict (artifact )
2025- for artifact in promote_inputs ["validated_step_outputs" ]
2147+ * [
2148+ ArtifactReference .from_dict (artifact )
2149+ for artifact in promote_inputs ["validated_step_outputs" ]
2150+ ],
2151+ release_promotion_contract_ref ,
20262152 ],
20272153 reuse_decision = "computed" ,
20282154 vol = pipeline_volume ,
0 commit comments