@@ -759,3 +759,77 @@ def test_clone_before_scheduling(self):
759759 f"'{ self .CLONE_STEP } ' (index { clone_idx } ) must come before "
760760 f"'{ self .SCHED_STEP } ' (index { sched_idx } ). Steps: { steps } "
761761 )
762+
763+
764+ class TestSyncBranchesCredentialOrdering :
765+ """Verify that Git credentials are configured before the merge/push step.
766+
767+ The sync-branches workflow merges the default branch into autoloop/*
768+ branches. Merge commits require a Git identity (user.name/user.email)
769+ and pushes/fetches need an authenticated remote URL. Both must be
770+ configured before the merge step runs.
771+ """
772+
773+ CRED_STEP = "Set up Git identity and authentication"
774+ MERGE_STEP = "Merge default branch into all autoloop program branches"
775+
776+ def _load_steps (self ):
777+ """Return the list of pre-step names from workflows/sync-branches.md."""
778+ import os
779+
780+ wf_path = os .path .join (os .path .dirname (__file__ ), ".." , "workflows" , "sync-branches.md" )
781+ with open (wf_path ) as f :
782+ content = f .read ()
783+ step_names = []
784+ for m in re .finditer (r'^\s*-\s*name:\s*(.+)$' , content , re .MULTILINE ):
785+ step_names .append (m .group (1 ).strip ())
786+ return step_names
787+
788+ def _load_lock_steps (self ):
789+ """Return the list of step names from .github/workflows/sync-branches.lock.yml."""
790+ import os
791+ import yaml
792+
793+ lock_path = os .path .join (
794+ os .path .dirname (__file__ ), ".." , ".github" , "workflows" , "sync-branches.lock.yml"
795+ )
796+ with open (lock_path ) as f :
797+ data = yaml .safe_load (f )
798+ # Collect step names from the 'agent' job
799+ steps = data .get ("jobs" , {}).get ("agent" , {}).get ("steps" , [])
800+ return [s .get ("name" , "" ) for s in steps if s .get ("name" )]
801+
802+ def test_cred_step_exists (self ):
803+ """A step that configures Git identity/auth must exist in the source."""
804+ steps = self ._load_steps ()
805+ assert self .CRED_STEP in steps , (
806+ f"Expected step '{ self .CRED_STEP } ' not found. Steps: { steps } "
807+ )
808+
809+ def test_creds_before_merge (self ):
810+ """The credential step must come before the merge step in the source."""
811+ steps = self ._load_steps ()
812+ cred_idx = steps .index (self .CRED_STEP )
813+ merge_idx = steps .index (self .MERGE_STEP )
814+ assert cred_idx < merge_idx , (
815+ f"'{ self .CRED_STEP } ' (index { cred_idx } ) must come before "
816+ f"'{ self .MERGE_STEP } ' (index { merge_idx } ). Steps: { steps } "
817+ )
818+
819+ def test_lock_creds_before_merge (self ):
820+ """In the compiled lock file, Configure Git credentials must come before the merge step."""
821+ steps = self ._load_lock_steps ()
822+ cred_names = [s for s in steps if "Configure Git credentials" in s ]
823+ assert cred_names , (
824+ f"No 'Configure Git credentials' step found in lock file. Steps: { steps } "
825+ )
826+ merge_names = [s for s in steps if "Merge default branch" in s ]
827+ assert merge_names , (
828+ f"No merge step found in lock file. Steps: { steps } "
829+ )
830+ cred_idx = steps .index (cred_names [0 ])
831+ merge_idx = steps .index (merge_names [0 ])
832+ assert cred_idx < merge_idx , (
833+ f"'Configure Git credentials' (index { cred_idx } ) must come before "
834+ f"merge step (index { merge_idx } ). Steps: { steps } "
835+ )
0 commit comments