@@ -548,6 +548,354 @@ func TestGitHubCommitEvent_SyncSourceRefresh_FileFiltering(t *testing.T) {
548548 }
549549}
550550
551+ // TestGitHubCommitEvent_Hydration_DrySource_WithAnnotation tests that drySource webhooks
552+ // with manifest-generate-paths annotation filter files based on annotation paths
553+ func TestGitHubCommitEvent_Hydration_DrySource_WithAnnotation (t * testing.T ) {
554+ hook := test .NewGlobal ()
555+ var patched bool
556+ reaction := func (action kubetesting.Action ) (handled bool , ret runtime.Object , err error ) {
557+ patchAction := action .(kubetesting.PatchAction )
558+ assert .Equal (t , "app-with-annotation" , patchAction .GetName ())
559+ patched = true
560+ return true , nil , nil
561+ }
562+
563+ // The test payload changes files under ksapps/ directory
564+ tests := []struct {
565+ name string
566+ annotation string
567+ drySourcePath string
568+ expectedRefresh bool
569+ }{
570+ {
571+ name : "annotation matches changed files - should hydrate" ,
572+ annotation : "." ,
573+ drySourcePath : "ksapps" ,
574+ expectedRefresh : true ,
575+ },
576+ {
577+ name : "annotation does not match changed files - should not hydrate" ,
578+ annotation : "." ,
579+ drySourcePath : "helm-charts" ,
580+ expectedRefresh : false ,
581+ },
582+ {
583+ name : "annotation with relative path matches - should hydrate" ,
584+ annotation : ".;../shared" ,
585+ drySourcePath : "ksapps" ,
586+ expectedRefresh : true ,
587+ },
588+ }
589+
590+ for _ , tt := range tests {
591+ t .Run (tt .name , func (t * testing.T ) {
592+ patched = false
593+ h := NewMockHandler (& reactorDef {"patch" , "applications" , reaction }, []string {}, & v1alpha1.Application {
594+ ObjectMeta : metav1.ObjectMeta {
595+ Name : "app-with-annotation" ,
596+ Namespace : "argocd" ,
597+ Annotations : map [string ]string {
598+ "argocd.argoproj.io/manifest-generate-paths" : tt .annotation ,
599+ },
600+ },
601+ Spec : v1alpha1.ApplicationSpec {
602+ SourceHydrator : & v1alpha1.SourceHydrator {
603+ DrySource : v1alpha1.DrySource {
604+ RepoURL : "https://github.com/jessesuen/test-repo" ,
605+ TargetRevision : "HEAD" , // Matches master branch from webhook event
606+ Path : tt .drySourcePath ,
607+ },
608+ SyncSource : v1alpha1.SyncSource {
609+ TargetBranch : "environments/dev" ,
610+ Path : "hydrated" ,
611+ },
612+ },
613+ },
614+ })
615+ req := httptest .NewRequest (http .MethodPost , "/api/webhook" , http .NoBody )
616+ req .Header .Set ("X-GitHub-Event" , "push" )
617+ // Use main branch event for drySource testing (matches drySource.TargetRevision)
618+ eventJSON , err := os .ReadFile ("testdata/github-commit-event.json" )
619+ require .NoError (t , err )
620+ req .Body = io .NopCloser (bytes .NewReader (eventJSON ))
621+ w := httptest .NewRecorder ()
622+ h .Handler (w , req )
623+ close (h .queue )
624+ h .Wait ()
625+ assert .Equal (t , http .StatusOK , w .Code )
626+ assert .Equal (t , tt .expectedRefresh , patched )
627+
628+ logMessages := make ([]string , 0 , len (hook .Entries ))
629+ for _ , entry := range hook .Entries {
630+ logMessages = append (logMessages , entry .Message )
631+ }
632+
633+ if tt .expectedRefresh {
634+ assert .Contains (t , logMessages , "webhook trigger refresh app to hydrate 'app-with-annotation'" )
635+ } else {
636+ assert .NotContains (t , logMessages , "webhook trigger refresh app to hydrate 'app-with-annotation'" )
637+ }
638+ hook .Reset ()
639+ })
640+ }
641+ }
642+
643+ // TestGitHubCommitEvent_Hydration_DrySource_NoAnnotation tests that drySource webhooks
644+ // without manifest-generate-paths annotation use the entire drySource path as default
645+ func TestGitHubCommitEvent_Hydration_DrySource_NoAnnotation (t * testing.T ) {
646+ hook := test .NewGlobal ()
647+ var patched bool
648+ reaction := func (action kubetesting.Action ) (handled bool , ret runtime.Object , err error ) {
649+ patchAction := action .(kubetesting.PatchAction )
650+ assert .Equal (t , "app-no-annotation" , patchAction .GetName ())
651+ patched = true
652+ return true , nil , nil
653+ }
654+
655+ // The test payload changes files under ksapps/ directory
656+ tests := []struct {
657+ name string
658+ drySourcePath string
659+ expectedRefresh bool
660+ }{
661+ {
662+ name : "drySource path matches changed files - should hydrate" ,
663+ drySourcePath : "ksapps" ,
664+ expectedRefresh : true ,
665+ },
666+ {
667+ name : "drySource path does not match changed files - should not hydrate" ,
668+ drySourcePath : "helm-charts" ,
669+ expectedRefresh : false ,
670+ },
671+ {
672+ name : "drySource at root path - should hydrate" ,
673+ drySourcePath : "." ,
674+ expectedRefresh : true ,
675+ },
676+ }
677+
678+ for _ , tt := range tests {
679+ t .Run (tt .name , func (t * testing.T ) {
680+ patched = false
681+ h := NewMockHandler (& reactorDef {"patch" , "applications" , reaction }, []string {}, & v1alpha1.Application {
682+ ObjectMeta : metav1.ObjectMeta {
683+ Name : "app-no-annotation" ,
684+ Namespace : "argocd" ,
685+ // No manifest-generate-paths annotation
686+ },
687+ Spec : v1alpha1.ApplicationSpec {
688+ SourceHydrator : & v1alpha1.SourceHydrator {
689+ DrySource : v1alpha1.DrySource {
690+ RepoURL : "https://github.com/jessesuen/test-repo" ,
691+ TargetRevision : "HEAD" , // Matches master branch from webhook event
692+ Path : tt .drySourcePath ,
693+ },
694+ SyncSource : v1alpha1.SyncSource {
695+ TargetBranch : "environments/dev" ,
696+ Path : "hydrated" ,
697+ },
698+ },
699+ },
700+ })
701+ req := httptest .NewRequest (http .MethodPost , "/api/webhook" , http .NoBody )
702+ req .Header .Set ("X-GitHub-Event" , "push" )
703+ // Use main branch event for drySource testing (matches drySource.TargetRevision)
704+ eventJSON , err := os .ReadFile ("testdata/github-commit-event.json" )
705+ require .NoError (t , err )
706+ req .Body = io .NopCloser (bytes .NewReader (eventJSON ))
707+ w := httptest .NewRecorder ()
708+ h .Handler (w , req )
709+ close (h .queue )
710+ h .Wait ()
711+ assert .Equal (t , http .StatusOK , w .Code )
712+ assert .Equal (t , tt .expectedRefresh , patched )
713+
714+ logMessages := make ([]string , 0 , len (hook .Entries ))
715+ for _ , entry := range hook .Entries {
716+ logMessages = append (logMessages , entry .Message )
717+ }
718+
719+ if tt .expectedRefresh {
720+ assert .Contains (t , logMessages , "webhook trigger refresh app to hydrate 'app-no-annotation'" )
721+ } else {
722+ assert .NotContains (t , logMessages , "webhook trigger refresh app to hydrate 'app-no-annotation'" )
723+ }
724+ hook .Reset ()
725+ })
726+ }
727+ }
728+
729+ // TestGitHubCommitEvent_Standard_WithAnnotation tests that standard apps (no hydration)
730+ // with manifest-generate-paths annotation only refresh when changed files match annotation paths
731+ func TestGitHubCommitEvent_Standard_WithAnnotation (t * testing.T ) {
732+ hook := test .NewGlobal ()
733+ var patched bool
734+ reaction := func (action kubetesting.Action ) (handled bool , ret runtime.Object , err error ) {
735+ patchAction := action .(kubetesting.PatchAction )
736+ assert .Equal (t , "standard-app-with-annotation" , patchAction .GetName ())
737+ patched = true
738+ return true , nil , nil
739+ }
740+
741+ // The test payload changes files under ksapps/ directory
742+ tests := []struct {
743+ name string
744+ annotation string
745+ sourcePath string
746+ expectedRefresh bool
747+ }{
748+ {
749+ name : "annotation matches changed files - should refresh" ,
750+ annotation : "." ,
751+ sourcePath : "ksapps" ,
752+ expectedRefresh : true ,
753+ },
754+ {
755+ name : "annotation does not match changed files - should not refresh" ,
756+ annotation : "." ,
757+ sourcePath : "helm-charts" ,
758+ expectedRefresh : false ,
759+ },
760+ {
761+ name : "annotation with multiple paths, one matches - should refresh" ,
762+ annotation : ".;../other" ,
763+ sourcePath : "ksapps" ,
764+ expectedRefresh : true ,
765+ },
766+ {
767+ name : "annotation at root matches all - should refresh" ,
768+ annotation : "." ,
769+ sourcePath : "." ,
770+ expectedRefresh : true ,
771+ },
772+ }
773+
774+ for _ , tt := range tests {
775+ t .Run (tt .name , func (t * testing.T ) {
776+ patched = false
777+ h := NewMockHandler (& reactorDef {"patch" , "applications" , reaction }, []string {}, & v1alpha1.Application {
778+ ObjectMeta : metav1.ObjectMeta {
779+ Name : "standard-app-with-annotation" ,
780+ Namespace : "argocd" ,
781+ Annotations : map [string ]string {
782+ "argocd.argoproj.io/manifest-generate-paths" : tt .annotation ,
783+ },
784+ },
785+ Spec : v1alpha1.ApplicationSpec {
786+ Sources : v1alpha1.ApplicationSources {
787+ {
788+ RepoURL : "https://github.com/jessesuen/test-repo" ,
789+ Path : tt .sourcePath ,
790+ TargetRevision : "HEAD" , // Matches the master branch from the webhook event
791+ },
792+ },
793+ },
794+ })
795+ req := httptest .NewRequest (http .MethodPost , "/api/webhook" , http .NoBody )
796+ req .Header .Set ("X-GitHub-Event" , "push" )
797+ // Use main branch event for standard app testing (matches source targetRevision: HEAD/master)
798+ eventJSON , err := os .ReadFile ("testdata/github-commit-event.json" )
799+ require .NoError (t , err )
800+ req .Body = io .NopCloser (bytes .NewReader (eventJSON ))
801+ w := httptest .NewRecorder ()
802+ h .Handler (w , req )
803+ close (h .queue )
804+ h .Wait ()
805+ assert .Equal (t , http .StatusOK , w .Code )
806+ assert .Equal (t , tt .expectedRefresh , patched )
807+
808+ logMessages := make ([]string , 0 , len (hook .Entries ))
809+ for _ , entry := range hook .Entries {
810+ logMessages = append (logMessages , entry .Message )
811+ }
812+
813+ if tt .expectedRefresh {
814+ assert .Contains (t , logMessages , "Requested app 'standard-app-with-annotation' refresh" )
815+ } else {
816+ assert .NotContains (t , logMessages , "Requested app 'standard-app-with-annotation' refresh" )
817+ }
818+ hook .Reset ()
819+ })
820+ }
821+ }
822+
823+ // TestGitHubCommitEvent_Standard_NoAnnotation tests that standard apps (no hydration)
824+ // without manifest-generate-paths annotation always refresh on any change (default behavior)
825+ func TestGitHubCommitEvent_Standard_NoAnnotation (t * testing.T ) {
826+ hook := test .NewGlobal ()
827+ var patched bool
828+ reaction := func (action kubetesting.Action ) (handled bool , ret runtime.Object , err error ) {
829+ patchAction := action .(kubetesting.PatchAction )
830+ assert .Equal (t , "standard-app-no-annotation" , patchAction .GetName ())
831+ patched = true
832+ return true , nil , nil
833+ }
834+
835+ // Test that regardless of which files change or which path is configured,
836+ // the app always refreshes when no annotation is present (default behavior)
837+ tests := []struct {
838+ name string
839+ sourcePath string
840+ }{
841+ {
842+ name : "source path matches changed files - should refresh" ,
843+ sourcePath : "ksapps" ,
844+ },
845+ {
846+ name : "source path does not match changed files - should still refresh" ,
847+ sourcePath : "helm-charts" ,
848+ },
849+ {
850+ name : "source at root - should refresh" ,
851+ sourcePath : "." ,
852+ },
853+ }
854+
855+ for _ , tt := range tests {
856+ t .Run (tt .name , func (t * testing.T ) {
857+ patched = false
858+ h := NewMockHandler (& reactorDef {"patch" , "applications" , reaction }, []string {}, & v1alpha1.Application {
859+ ObjectMeta : metav1.ObjectMeta {
860+ Name : "standard-app-no-annotation" ,
861+ Namespace : "argocd" ,
862+ // No manifest-generate-paths annotation
863+ },
864+ Spec : v1alpha1.ApplicationSpec {
865+ Sources : v1alpha1.ApplicationSources {
866+ {
867+ RepoURL : "https://github.com/jessesuen/test-repo" ,
868+ Path : tt .sourcePath ,
869+ TargetRevision : "HEAD" , // Matches the master branch from the webhook event
870+ },
871+ },
872+ },
873+ })
874+ req := httptest .NewRequest (http .MethodPost , "/api/webhook" , http .NoBody )
875+ req .Header .Set ("X-GitHub-Event" , "push" )
876+ // Use main branch event for standard app testing (matches source targetRevision: HEAD/master)
877+ eventJSON , err := os .ReadFile ("testdata/github-commit-event.json" )
878+ require .NoError (t , err )
879+ req .Body = io .NopCloser (bytes .NewReader (eventJSON ))
880+ w := httptest .NewRecorder ()
881+ h .Handler (w , req )
882+ close (h .queue )
883+ h .Wait ()
884+ assert .Equal (t , http .StatusOK , w .Code )
885+ // Should always refresh regardless of path
886+ assert .True (t , patched , "expected app to refresh but it didn't" )
887+
888+ logMessages := make ([]string , 0 , len (hook .Entries ))
889+ for _ , entry := range hook .Entries {
890+ logMessages = append (logMessages , entry .Message )
891+ }
892+
893+ assert .Contains (t , logMessages , "Requested app 'standard-app-no-annotation' refresh" )
894+ hook .Reset ()
895+ })
896+ }
897+ }
898+
551899func TestGitHubTagEvent (t * testing.T ) {
552900 hook := test .NewGlobal ()
553901 h := NewMockHandler (nil , []string {})
0 commit comments