@@ -597,12 +597,29 @@ class TaskQueueBuildIdCohort:
597597 sdk_versions : list [str ]
598598 last_heartbeat_at : str | None = None
599599 first_seen_at : str | None = None
600+ drain_intent : str | None = None
601+ drained_at : str | None = None
602+ promoted_at : str | None = None
603+ rolled_back_at : str | None = None
604+ new_start_selected : bool = False
605+ workflow_definition_fingerprint_count : int = 0
606+ workflow_definition_fingerprint_conflicts : list [dict [str , Any ]] | None = None
600607 raw : dict [str , Any ] | None = None
601608
602609 @classmethod
603610 def from_dict (cls , data : dict [str , Any ]) -> TaskQueueBuildIdCohort :
604611 runtimes = data .get ("runtimes" )
605612 sdk_versions = data .get ("sdk_versions" )
613+ fingerprint_conflicts_raw = data .get ("workflow_definition_fingerprint_conflicts" )
614+ fingerprint_conflicts : list [dict [str , Any ]] | None = None
615+ if isinstance (fingerprint_conflicts_raw , list ):
616+ fingerprint_conflicts = []
617+ for item in fingerprint_conflicts_raw :
618+ if not isinstance (item , dict ):
619+ continue
620+ fingerprint_conflicts .append (
621+ {str (key ): value for key , value in item .items () if isinstance (key , str )}
622+ )
606623 return cls (
607624 build_id = data .get ("build_id" ),
608625 rollout_status = str (data .get ("rollout_status" ) or "" ),
@@ -614,6 +631,15 @@ def from_dict(cls, data: dict[str, Any]) -> TaskQueueBuildIdCohort:
614631 sdk_versions = [v for v in sdk_versions if isinstance (v , str )] if isinstance (sdk_versions , list ) else [],
615632 last_heartbeat_at = data .get ("last_heartbeat_at" ),
616633 first_seen_at = data .get ("first_seen_at" ),
634+ drain_intent = data .get ("drain_intent" ) if isinstance (data .get ("drain_intent" ), str ) else None ,
635+ drained_at = data .get ("drained_at" ) if isinstance (data .get ("drained_at" ), str ) else None ,
636+ promoted_at = data .get ("promoted_at" ) if isinstance (data .get ("promoted_at" ), str ) else None ,
637+ rolled_back_at = data .get ("rolled_back_at" ) if isinstance (data .get ("rolled_back_at" ), str ) else None ,
638+ new_start_selected = bool (data .get ("new_start_selected" )),
639+ workflow_definition_fingerprint_count = int (
640+ data .get ("workflow_definition_fingerprint_count" ) or 0
641+ ),
642+ workflow_definition_fingerprint_conflicts = fingerprint_conflicts ,
617643 raw = data ,
618644 )
619645
@@ -650,28 +676,41 @@ def from_dict(cls, data: dict[str, Any]) -> TaskQueueBuildIdRollout:
650676class TaskQueueBuildIdRolloutState :
651677 """Operator-recorded drain intent for one ``(task_queue, build_id)`` cohort.
652678
653- Returned by ``drain_task_queue_build_id`` and ``resume_task_queue_build_id``.
679+ Returned by ``drain_task_queue_build_id``,
680+ ``promote_task_queue_build_id``, and ``resume_task_queue_build_id``.
654681 ``build_id`` is ``None`` for the unversioned cohort (workers registered
655682 without a build identifier). ``drain_intent`` is ``"active"`` or
656683 ``"draining"``. ``drained_at`` is set only when ``drain_intent`` is
657684 ``"draining"``; repeated drains do not shift the timestamp.
685+ ``promoted_at`` and ``new_start_selected`` identify the cohort currently
686+ selected for fresh workflow starts.
658687 """
659688
660689 namespace : str | None
661690 task_queue : str
662691 build_id : str | None
663692 drain_intent : str
664693 drained_at : str | None
694+ promoted_at : str | None = None
695+ rolled_back_at : str | None = None
696+ new_start_selected : bool = False
697+ deployment : dict [str , Any ] | None = None
665698 raw : dict [str , Any ] | None = None
666699
667700 @classmethod
668701 def from_dict (cls , data : dict [str , Any ]) -> TaskQueueBuildIdRolloutState :
702+ deployment_raw = data .get ("deployment" )
703+ deployment = dict (deployment_raw ) if isinstance (deployment_raw , dict ) else None
669704 return cls (
670705 namespace = data .get ("namespace" ),
671706 task_queue = str (data .get ("task_queue" ) or "" ),
672707 build_id = data .get ("build_id" ) if isinstance (data .get ("build_id" ), str ) else None ,
673708 drain_intent = str (data .get ("drain_intent" ) or "" ),
674709 drained_at = data .get ("drained_at" ) if isinstance (data .get ("drained_at" ), str ) else None ,
710+ promoted_at = data .get ("promoted_at" ) if isinstance (data .get ("promoted_at" ), str ) else None ,
711+ rolled_back_at = data .get ("rolled_back_at" ) if isinstance (data .get ("rolled_back_at" ), str ) else None ,
712+ new_start_selected = bool (data .get ("new_start_selected" )),
713+ deployment = deployment ,
675714 raw = data ,
676715 )
677716
@@ -1895,6 +1934,24 @@ async def drain_task_queue_build_id(
18951934 action = "drain" ,
18961935 )
18971936
1937+ async def promote_task_queue_build_id (
1938+ self ,
1939+ task_queue : str ,
1940+ build_id : str | None ,
1941+ ) -> TaskQueueBuildIdRolloutState :
1942+ """Select a build-id cohort for fresh workflow starts on a task queue.
1943+
1944+ New workflow starts pin to ``build_id`` after promotion. Existing
1945+ workflow runs keep their stamped compatibility marker and continue
1946+ routing only to compatible workers. Pass ``None`` to promote the
1947+ unversioned cohort.
1948+ """
1949+ return await self ._mutate_task_queue_build_id_rollout (
1950+ task_queue ,
1951+ build_id ,
1952+ action = "promote" ,
1953+ )
1954+
18981955 async def resume_task_queue_build_id (
18991956 self ,
19001957 task_queue : str ,
0 commit comments