@@ -594,3 +594,252 @@ runs:
594594 assert .Equal (t , "koi" , actionMetadata .Runs .Steps [0 ].With [0 ].Value )
595595 assert .Equal (t , 17 , actionMetadata .Runs .Steps [0 ].Lines ["uses" ])
596596}
597+
598+ // TestGithubActionsWorkflowWithAnchors tests YAML 1.2 anchor support
599+ // GitHub Actions now supports YAML anchors as of 2025-09-18
600+ // https://github.blog/changelog/2025-09-18-actions-yaml-anchors-and-non-public-workflow-templates/
601+ func TestGithubActionsWorkflowWithAnchors (t * testing.T ) {
602+ t .Run ("simple anchor and alias" , func (t * testing.T ) {
603+ workflow := `
604+ name: CI
605+ on: push
606+
607+ jobs:
608+ build: &build_template
609+ runs-on: ubuntu-latest
610+ steps:
611+ - uses: actions/checkout@v4
612+
613+ test:
614+ <<: *build_template
615+ steps:
616+ - uses: actions/checkout@v4
617+ - run: npm test
618+ `
619+ var wf GithubActionsWorkflow
620+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
621+ require .NoError (t , err )
622+ assert .Equal (t , "CI" , wf .Name )
623+ assert .Len (t , wf .Jobs , 2 )
624+
625+ // Verify the first job (with anchor definition)
626+ assert .Equal (t , "build" , wf .Jobs [0 ].ID )
627+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [0 ].RunsOn )
628+ assert .Len (t , wf .Jobs [0 ].Steps , 1 )
629+ assert .Equal (t , "actions/checkout@v4" , wf .Jobs [0 ].Steps [0 ].Uses )
630+
631+ // Verify the second job inherited runs-on from the anchor
632+ assert .Equal (t , "test" , wf .Jobs [1 ].ID )
633+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [1 ].RunsOn )
634+ assert .Len (t , wf .Jobs [1 ].Steps , 2 )
635+ assert .Equal (t , "actions/checkout@v4" , wf .Jobs [1 ].Steps [0 ].Uses )
636+ assert .Equal (t , "npm test" , wf .Jobs [1 ].Steps [1 ].Run )
637+ })
638+
639+ t .Run ("anchor for environment configuration" , func (t * testing.T ) {
640+ workflow := `
641+ name: Deploy
642+ on: push
643+
644+ jobs:
645+ deploy-staging:
646+ runs-on: ubuntu-latest
647+ environment: &env_config
648+ name: staging
649+ url: https://staging.example.com
650+ steps:
651+ - run: echo "deploying"
652+
653+ deploy-prod:
654+ runs-on: ubuntu-latest
655+ environment:
656+ <<: *env_config
657+ name: production
658+ url: https://prod.example.com
659+ steps:
660+ - run: echo "deploying"
661+ `
662+ var wf GithubActionsWorkflow
663+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
664+ require .NoError (t , err )
665+ assert .Len (t , wf .Jobs , 2 )
666+ assert .Equal (t , "staging" , wf .Jobs [0 ].Environment [0 ].Name )
667+ assert .Equal (t , "https://staging.example.com" , wf .Jobs [0 ].Environment [0 ].Url )
668+ assert .Equal (t , "production" , wf .Jobs [1 ].Environment [0 ].Name )
669+ assert .Equal (t , "https://prod.example.com" , wf .Jobs [1 ].Environment [0 ].Url )
670+ })
671+
672+ t .Run ("anchor for steps configuration" , func (t * testing.T ) {
673+ workflow := `
674+ name: Test
675+ on: push
676+
677+ jobs:
678+ test-node-14:
679+ runs-on: ubuntu-latest
680+ steps: &test_steps
681+ - uses: actions/checkout@v4
682+ - uses: actions/setup-node@v4
683+ with:
684+ node-version: '14'
685+ - run: npm test
686+
687+ test-node-16:
688+ runs-on: ubuntu-latest
689+ steps: *test_steps
690+ `
691+ var wf GithubActionsWorkflow
692+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
693+ require .NoError (t , err )
694+ assert .Len (t , wf .Jobs , 2 )
695+
696+ // Verify both jobs have the same steps from the anchor
697+ assert .Len (t , wf .Jobs [0 ].Steps , 3 )
698+ assert .Equal (t , "actions/checkout@v4" , wf .Jobs [0 ].Steps [0 ].Uses )
699+ assert .Equal (t , "actions/setup-node@v4" , wf .Jobs [0 ].Steps [1 ].Uses )
700+ assert .Equal (t , "npm test" , wf .Jobs [0 ].Steps [2 ].Run )
701+
702+ // Verify the second job uses the anchor and has identical steps
703+ assert .Len (t , wf .Jobs [1 ].Steps , 3 )
704+ assert .Equal (t , "actions/checkout@v4" , wf .Jobs [1 ].Steps [0 ].Uses )
705+ assert .Equal (t , "actions/setup-node@v4" , wf .Jobs [1 ].Steps [1 ].Uses )
706+ assert .Equal (t , "npm test" , wf .Jobs [1 ].Steps [2 ].Run )
707+ })
708+
709+ t .Run ("anchor for permissions" , func (t * testing.T ) {
710+ workflow := `
711+ name: Security
712+ on: push
713+
714+ jobs:
715+ scan:
716+ runs-on: ubuntu-latest
717+ permissions: &security_perms
718+ contents: read
719+ security-events: write
720+ steps:
721+ - run: echo "scanning"
722+
723+ report:
724+ runs-on: ubuntu-latest
725+ permissions: *security_perms
726+ steps:
727+ - run: echo "reporting"
728+ `
729+ var wf GithubActionsWorkflow
730+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
731+ require .NoError (t , err )
732+ assert .Len (t , wf .Jobs , 2 )
733+ assert .Len (t , wf .Jobs [0 ].Permissions , 2 )
734+ assert .Contains (t , wf .Jobs [0 ].Permissions , GithubActionsPermission {Scope : "contents" , Permission : "read" })
735+ assert .Contains (t , wf .Jobs [0 ].Permissions , GithubActionsPermission {Scope : "security-events" , Permission : "write" })
736+ assert .Equal (t , wf .Jobs [0 ].Permissions , wf .Jobs [1 ].Permissions )
737+ })
738+
739+ t .Run ("multiple anchors in same workflow" , func (t * testing.T ) {
740+ workflow := `
741+ name: Multi
742+ on: push
743+
744+ jobs:
745+ job1:
746+ runs-on: &runner ubuntu-latest
747+ container: &container_image alpine:latest
748+ steps:
749+ - run: echo "test"
750+
751+ job2:
752+ runs-on: *runner
753+ container: *container_image
754+ steps:
755+ - run: echo "test2"
756+ `
757+ var wf GithubActionsWorkflow
758+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
759+ require .NoError (t , err )
760+ assert .Len (t , wf .Jobs , 2 )
761+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [0 ].RunsOn )
762+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [1 ].RunsOn )
763+ assert .Equal (t , "alpine:latest" , wf .Jobs [0 ].Container .Image )
764+ assert .Equal (t , "alpine:latest" , wf .Jobs [1 ].Container .Image )
765+ })
766+
767+ t .Run ("anchor for env variables" , func (t * testing.T ) {
768+ workflow := `
769+ name: Env Test
770+ on: push
771+
772+ jobs:
773+ build:
774+ runs-on: ubuntu-latest
775+ env: &common_env
776+ NODE_ENV: production
777+ CI: true
778+ steps:
779+ - run: echo "build"
780+
781+ test:
782+ runs-on: ubuntu-latest
783+ env: *common_env
784+ steps:
785+ - run: echo "test"
786+ `
787+ var wf GithubActionsWorkflow
788+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
789+ require .NoError (t , err )
790+ assert .Len (t , wf .Jobs , 2 )
791+
792+ // Verify the first job has the anchor env vars
793+ assert .Len (t , wf .Jobs [0 ].Env , 2 )
794+ assert .Contains (t , wf .Jobs [0 ].Env , GithubActionsEnv {Name : "NODE_ENV" , Value : "production" })
795+ assert .Contains (t , wf .Jobs [0 ].Env , GithubActionsEnv {Name : "CI" , Value : "true" })
796+
797+ // Verify the second job reuses the same env vars via alias
798+ assert .Len (t , wf .Jobs [1 ].Env , 2 )
799+ assert .Contains (t , wf .Jobs [1 ].Env , GithubActionsEnv {Name : "NODE_ENV" , Value : "production" })
800+ assert .Contains (t , wf .Jobs [1 ].Env , GithubActionsEnv {Name : "CI" , Value : "true" })
801+ })
802+
803+ t .Run ("complex nested anchor with merge keys" , func (t * testing.T ) {
804+ workflow := `
805+ name: Complex
806+ on: push
807+
808+ jobs:
809+ base: &base_job
810+ runs-on: ubuntu-latest
811+ permissions:
812+ contents: read
813+ steps:
814+ - uses: actions/checkout@v4
815+
816+ extended:
817+ <<: *base_job
818+ permissions:
819+ contents: write
820+ issues: write
821+ steps:
822+ - uses: actions/checkout@v4
823+ - run: npm build
824+ `
825+ var wf GithubActionsWorkflow
826+ err := yaml .Unmarshal ([]byte (workflow ), & wf )
827+ require .NoError (t , err )
828+ assert .Len (t , wf .Jobs , 2 )
829+
830+ // Verify the base job
831+ assert .Equal (t , "base" , wf .Jobs [0 ].ID )
832+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [0 ].RunsOn )
833+ assert .Len (t , wf .Jobs [0 ].Permissions , 1 )
834+ assert .Contains (t , wf .Jobs [0 ].Permissions , GithubActionsPermission {Scope : "contents" , Permission : "read" })
835+ assert .Len (t , wf .Jobs [0 ].Steps , 1 )
836+
837+ // Verify the extended job inherited runs-on but overrode permissions
838+ assert .Equal (t , "extended" , wf .Jobs [1 ].ID )
839+ assert .Equal (t , GithubActionsJobRunsOn {"ubuntu-latest" }, wf .Jobs [1 ].RunsOn )
840+ assert .Len (t , wf .Jobs [1 ].Permissions , 2 )
841+ assert .Contains (t , wf .Jobs [1 ].Permissions , GithubActionsPermission {Scope : "contents" , Permission : "write" })
842+ assert .Contains (t , wf .Jobs [1 ].Permissions , GithubActionsPermission {Scope : "issues" , Permission : "write" })
843+ assert .Len (t , wf .Jobs [1 ].Steps , 2 )
844+ })
845+ }
0 commit comments