@@ -675,6 +675,283 @@ describe('git-auth-helper tests', () => {
675675 expect ( gitConfigContent . indexOf ( 'http.' ) ) . toBeLessThan ( 0 )
676676 } )
677677
678+ const removeAuth_removesV6StyleCredentials =
679+ 'removeAuth removes v6 style credentials'
680+ it ( removeAuth_removesV6StyleCredentials , async ( ) => {
681+ // Arrange
682+ await setup ( removeAuth_removesV6StyleCredentials )
683+ const authHelper = gitAuthHelper . createAuthHelper ( git , settings )
684+ await authHelper . configureAuth ( )
685+
686+ // Manually create v6-style credentials that would be left by v6
687+ const credentialsFileName =
688+ 'git-credentials-12345678-1234-1234-1234-123456789abc.config'
689+ const credentialsFilePath = path . join ( runnerTemp , credentialsFileName )
690+ const basicCredential = Buffer . from (
691+ `x-access-token:${ settings . authToken } ` ,
692+ 'utf8'
693+ ) . toString ( 'base64' )
694+ const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${ basicCredential } \n`
695+ await fs . promises . writeFile ( credentialsFilePath , credentialsContent )
696+
697+ // Add includeIf entries to local git config (simulating v6 configuration)
698+ const hostGitDir = path . join ( workspace , '.git' ) . replace ( / \\ / g, '/' )
699+ await fs . promises . appendFile (
700+ localGitConfigPath ,
701+ `[includeIf "gitdir:${ hostGitDir } /"]\n\tpath = ${ credentialsFilePath } \n`
702+ )
703+ await fs . promises . appendFile (
704+ localGitConfigPath ,
705+ `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${ credentialsFileName } \n`
706+ )
707+
708+ // Verify v6 style config exists
709+ let gitConfigContent = (
710+ await fs . promises . readFile ( localGitConfigPath )
711+ ) . toString ( )
712+ expect ( gitConfigContent . indexOf ( 'includeIf' ) ) . toBeGreaterThanOrEqual ( 0 )
713+ expect (
714+ gitConfigContent . indexOf ( credentialsFilePath )
715+ ) . toBeGreaterThanOrEqual ( 0 )
716+ await fs . promises . stat ( credentialsFilePath ) // Verify file exists
717+
718+ // Mock the git methods to handle v6 cleanup
719+ const mockTryGetConfigKeys = git . tryGetConfigKeys as jest . Mock < any , any >
720+ mockTryGetConfigKeys . mockResolvedValue ( [
721+ `includeIf.gitdir:${ hostGitDir } /.path` ,
722+ 'includeIf.gitdir:/github/workspace/.git/.path'
723+ ] )
724+
725+ const mockTryGetConfigValues = git . tryGetConfigValues as jest . Mock < any , any >
726+ mockTryGetConfigValues . mockImplementation ( async ( key : string ) => {
727+ if ( key === `includeIf.gitdir:${ hostGitDir } /.path` ) {
728+ return [ credentialsFilePath ]
729+ }
730+ if ( key === 'includeIf.gitdir:/github/workspace/.git/.path' ) {
731+ return [ `/github/runner_temp/${ credentialsFileName } ` ]
732+ }
733+ return [ ]
734+ } )
735+
736+ const mockTryConfigUnsetValue = git . tryConfigUnsetValue as jest . Mock <
737+ any ,
738+ any
739+ >
740+ mockTryConfigUnsetValue . mockImplementation (
741+ async (
742+ key : string ,
743+ value : string ,
744+ globalConfig ?: boolean ,
745+ configPath ?: string
746+ ) => {
747+ const targetPath = configPath || localGitConfigPath
748+ let content = await fs . promises . readFile ( targetPath , 'utf8' )
749+ // Remove the includeIf section
750+ const lines = content
751+ . split ( '\n' )
752+ . filter ( line => ! line . includes ( 'includeIf' ) && ! line . includes ( value ) )
753+ await fs . promises . writeFile ( targetPath , lines . join ( '\n' ) )
754+ return true
755+ }
756+ )
757+
758+ // Act
759+ await authHelper . removeAuth ( )
760+
761+ // Assert includeIf entries removed from local git config
762+ gitConfigContent = (
763+ await fs . promises . readFile ( localGitConfigPath )
764+ ) . toString ( )
765+ expect ( gitConfigContent . indexOf ( 'includeIf' ) ) . toBeLessThan ( 0 )
766+ expect ( gitConfigContent . indexOf ( credentialsFilePath ) ) . toBeLessThan ( 0 )
767+
768+ // Assert credentials config file deleted
769+ try {
770+ await fs . promises . stat ( credentialsFilePath )
771+ throw new Error ( 'Credentials file should have been deleted' )
772+ } catch ( err ) {
773+ if ( ( err as any ) ?. code !== 'ENOENT' ) {
774+ throw err
775+ }
776+ }
777+ } )
778+
779+ const removeAuth_removesV6StyleCredentialsFromSubmodules =
780+ 'removeAuth removes v6 style credentials from submodules'
781+ it ( removeAuth_removesV6StyleCredentialsFromSubmodules , async ( ) => {
782+ // Arrange
783+ await setup ( removeAuth_removesV6StyleCredentialsFromSubmodules )
784+
785+ // Create fake submodule config paths
786+ const submodule1Dir = path . join ( workspace , '.git' , 'modules' , 'submodule-1' )
787+ const submodule1ConfigPath = path . join ( submodule1Dir , 'config' )
788+ await fs . promises . mkdir ( submodule1Dir , { recursive : true } )
789+ await fs . promises . writeFile ( submodule1ConfigPath , '' )
790+
791+ const authHelper = gitAuthHelper . createAuthHelper ( git , settings )
792+ await authHelper . configureAuth ( )
793+
794+ // Create v6-style credentials file
795+ const credentialsFileName =
796+ 'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
797+ const credentialsFilePath = path . join ( runnerTemp , credentialsFileName )
798+ const basicCredential = Buffer . from (
799+ `x-access-token:${ settings . authToken } ` ,
800+ 'utf8'
801+ ) . toString ( 'base64' )
802+ const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${ basicCredential } \n`
803+ await fs . promises . writeFile ( credentialsFilePath , credentialsContent )
804+
805+ // Add includeIf entries to submodule config
806+ const submodule1GitDir = submodule1Dir . replace ( / \\ / g, '/' )
807+ await fs . promises . appendFile (
808+ submodule1ConfigPath ,
809+ `[includeIf "gitdir:${ submodule1GitDir } /"]\n\tpath = ${ credentialsFilePath } \n`
810+ )
811+
812+ // Verify submodule config has includeIf entry
813+ let submoduleConfigContent = (
814+ await fs . promises . readFile ( submodule1ConfigPath )
815+ ) . toString ( )
816+ expect ( submoduleConfigContent . indexOf ( 'includeIf' ) ) . toBeGreaterThanOrEqual (
817+ 0
818+ )
819+ expect (
820+ submoduleConfigContent . indexOf ( credentialsFilePath )
821+ ) . toBeGreaterThanOrEqual ( 0 )
822+
823+ // Mock getSubmoduleConfigPaths
824+ const mockGetSubmoduleConfigPaths =
825+ git . getSubmoduleConfigPaths as jest . Mock < any , any >
826+ mockGetSubmoduleConfigPaths . mockResolvedValue ( [ submodule1ConfigPath ] )
827+
828+ // Mock tryGetConfigKeys for submodule
829+ const mockTryGetConfigKeys = git . tryGetConfigKeys as jest . Mock < any , any >
830+ mockTryGetConfigKeys . mockImplementation (
831+ async ( pattern : string , globalConfig ?: boolean , configPath ?: string ) => {
832+ if ( configPath === submodule1ConfigPath ) {
833+ return [ `includeIf.gitdir:${ submodule1GitDir } /.path` ]
834+ }
835+ return [ ]
836+ }
837+ )
838+
839+ // Mock tryGetConfigValues for submodule
840+ const mockTryGetConfigValues = git . tryGetConfigValues as jest . Mock < any , any >
841+ mockTryGetConfigValues . mockImplementation (
842+ async ( key : string , globalConfig ?: boolean , configPath ?: string ) => {
843+ if (
844+ configPath === submodule1ConfigPath &&
845+ key === `includeIf.gitdir:${ submodule1GitDir } /.path`
846+ ) {
847+ return [ credentialsFilePath ]
848+ }
849+ return [ ]
850+ }
851+ )
852+
853+ // Mock tryConfigUnsetValue for submodule
854+ const mockTryConfigUnsetValue = git . tryConfigUnsetValue as jest . Mock <
855+ any ,
856+ any
857+ >
858+ mockTryConfigUnsetValue . mockImplementation (
859+ async (
860+ key : string ,
861+ value : string ,
862+ globalConfig ?: boolean ,
863+ configPath ?: string
864+ ) => {
865+ const targetPath = configPath || localGitConfigPath
866+ let content = await fs . promises . readFile ( targetPath , 'utf8' )
867+ const lines = content
868+ . split ( '\n' )
869+ . filter ( line => ! line . includes ( 'includeIf' ) && ! line . includes ( value ) )
870+ await fs . promises . writeFile ( targetPath , lines . join ( '\n' ) )
871+ return true
872+ }
873+ )
874+
875+ // Act
876+ await authHelper . removeAuth ( )
877+
878+ // Assert submodule includeIf entries removed
879+ submoduleConfigContent = (
880+ await fs . promises . readFile ( submodule1ConfigPath )
881+ ) . toString ( )
882+ expect ( submoduleConfigContent . indexOf ( 'includeIf' ) ) . toBeLessThan ( 0 )
883+ expect ( submoduleConfigContent . indexOf ( credentialsFilePath ) ) . toBeLessThan ( 0 )
884+
885+ // Assert credentials file deleted
886+ try {
887+ await fs . promises . stat ( credentialsFilePath )
888+ throw new Error ( 'Credentials file should have been deleted' )
889+ } catch ( err ) {
890+ if ( ( err as any ) ?. code !== 'ENOENT' ) {
891+ throw err
892+ }
893+ }
894+ } )
895+
896+ const removeAuth_skipsV6CleanupWhenEnvVarSet =
897+ 'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
898+ it ( removeAuth_skipsV6CleanupWhenEnvVarSet , async ( ) => {
899+ // Arrange
900+ await setup ( removeAuth_skipsV6CleanupWhenEnvVarSet )
901+
902+ // Set the skip environment variable
903+ process . env [ 'ACTIONS_CHECKOUT_SKIP_V6_CLEANUP' ] = '1'
904+
905+ const authHelper = gitAuthHelper . createAuthHelper ( git , settings )
906+ await authHelper . configureAuth ( )
907+
908+ // Create v6-style credentials file in RUNNER_TEMP
909+ const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
910+ const credentialsFilePath = path . join ( runnerTemp , credentialsFileName )
911+ const credentialsContent =
912+ '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
913+ await fs . promises . writeFile ( credentialsFilePath , credentialsContent )
914+
915+ // Add includeIf section to local git config (separate from http.* config)
916+ const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${ credentialsFilePath } \n`
917+ await fs . promises . appendFile ( localGitConfigPath , includeIfSection )
918+
919+ // Verify v6 style config exists
920+ let gitConfigContent = (
921+ await fs . promises . readFile ( localGitConfigPath )
922+ ) . toString ( )
923+ expect ( gitConfigContent . indexOf ( 'includeIf' ) ) . toBeGreaterThanOrEqual ( 0 )
924+ await fs . promises . stat ( credentialsFilePath ) // Verify file exists
925+
926+ // Act
927+ await authHelper . removeAuth ( )
928+
929+ // Assert v5 cleanup still happened (http.* removed)
930+ gitConfigContent = (
931+ await fs . promises . readFile ( localGitConfigPath )
932+ ) . toString ( )
933+ expect (
934+ gitConfigContent . indexOf ( 'http.https://github.com/.extraheader' )
935+ ) . toBeLessThan ( 0 )
936+
937+ // Assert v6 cleanup was skipped - includeIf should still be present
938+ expect ( gitConfigContent . indexOf ( 'includeIf' ) ) . toBeGreaterThanOrEqual ( 0 )
939+ expect (
940+ gitConfigContent . indexOf ( credentialsFilePath )
941+ ) . toBeGreaterThanOrEqual ( 0 )
942+
943+ // Assert credentials file still exists (wasn't deleted)
944+ await fs . promises . stat ( credentialsFilePath ) // File should still exist
945+
946+ // Assert debug message was logged
947+ expect ( core . debug ) . toHaveBeenCalledWith (
948+ 'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
949+ )
950+
951+ // Cleanup
952+ delete process . env [ 'ACTIONS_CHECKOUT_SKIP_V6_CLEANUP' ]
953+ } )
954+
678955 const removeGlobalConfig_removesOverride =
679956 'removeGlobalConfig removes override'
680957 it ( removeGlobalConfig_removesOverride , async ( ) => {
@@ -796,6 +1073,18 @@ async function setup(testName: string): Promise<void> {
7961073 ) ,
7971074 tryDisableAutomaticGarbageCollection : jest . fn ( ) ,
7981075 tryGetFetchUrl : jest . fn ( ) ,
1076+ getSubmoduleConfigPaths : jest . fn ( async ( ) => {
1077+ return [ ]
1078+ } ) ,
1079+ tryConfigUnsetValue : jest . fn ( async ( ) => {
1080+ return true
1081+ } ) ,
1082+ tryGetConfigValues : jest . fn ( async ( ) => {
1083+ return [ ]
1084+ } ) ,
1085+ tryGetConfigKeys : jest . fn ( async ( ) => {
1086+ return [ ]
1087+ } ) ,
7991088 tryReset : jest . fn ( ) ,
8001089 version : jest . fn ( )
8011090 }
0 commit comments