mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 02:14:20 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			500 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			500 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as assert from 'assert'
 | |
| import * as core from '@actions/core'
 | |
| import * as exec from '@actions/exec'
 | |
| import * as fs from 'fs'
 | |
| import * as io from '@actions/io'
 | |
| import * as os from 'os'
 | |
| import * as path from 'path'
 | |
| import * as regexpHelper from './regexp-helper'
 | |
| import * as stateHelper from './state-helper'
 | |
| import * as urlHelper from './url-helper'
 | |
| import {v4 as uuid} from 'uuid'
 | |
| import {IGitCommandManager} from './git-command-manager'
 | |
| import {IGitSourceSettings} from './git-source-settings'
 | |
| 
 | |
| const IS_WINDOWS = process.platform === 'win32'
 | |
| const SSH_COMMAND_KEY = 'core.sshCommand'
 | |
| 
 | |
| export interface IGitAuthHelper {
 | |
|   configureAuth(): Promise<void>
 | |
|   configureGlobalAuth(): Promise<void>
 | |
|   configureSubmoduleAuth(): Promise<void>
 | |
|   configureTempGlobalConfig(): Promise<string>
 | |
|   removeAuth(): Promise<void>
 | |
|   removeGlobalConfig(): Promise<void>
 | |
| }
 | |
| 
 | |
| export function createAuthHelper(
 | |
|   git: IGitCommandManager,
 | |
|   settings?: IGitSourceSettings
 | |
| ): IGitAuthHelper {
 | |
|   return new GitAuthHelper(git, settings)
 | |
| }
 | |
| 
 | |
| class GitAuthHelper {
 | |
|   private readonly git: IGitCommandManager
 | |
|   private readonly settings: IGitSourceSettings
 | |
|   private readonly tokenConfigKey: string
 | |
|   private readonly tokenConfigValue: string
 | |
|   private readonly tokenPlaceholderConfigValue: string
 | |
|   private readonly insteadOfKey: string
 | |
|   private readonly insteadOfValues: string[] = []
 | |
|   private sshCommand = ''
 | |
|   private sshKeyPath = ''
 | |
|   private sshKnownHostsPath = ''
 | |
|   private temporaryHomePath = ''
 | |
|   private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
 | |
|   private credentialsIncludeKeys: string[] = [] // Track includeIf/include config keys for cleanup
 | |
| 
 | |
|   constructor(
 | |
|     gitCommandManager: IGitCommandManager,
 | |
|     gitSourceSettings: IGitSourceSettings | undefined
 | |
|   ) {
 | |
|     this.git = gitCommandManager
 | |
|     this.settings = gitSourceSettings || ({} as unknown as IGitSourceSettings)
 | |
| 
 | |
|     // Token auth header
 | |
|     const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
 | |
|     this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
 | |
|     const basicCredential = Buffer.from(
 | |
|       `x-access-token:${this.settings.authToken}`,
 | |
|       'utf8'
 | |
|     ).toString('base64')
 | |
|     core.setSecret(basicCredential)
 | |
|     this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
 | |
|     this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
 | |
| 
 | |
|     // Instead of SSH URL
 | |
|     this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf` // "origin" is SCHEME://HOSTNAME[:PORT]
 | |
|     this.insteadOfValues.push(`git@${serverUrl.hostname}:`)
 | |
|     if (this.settings.workflowOrganizationId) {
 | |
|       this.insteadOfValues.push(
 | |
|         `org-${this.settings.workflowOrganizationId}@github.com:`
 | |
|       )
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async configureAuth(): Promise<void> {
 | |
|     // Remove possible previous values
 | |
|     await this.removeAuth()
 | |
| 
 | |
|     // Configure new values
 | |
|     await this.configureSsh()
 | |
|     await this.configureToken()
 | |
|   }
 | |
| 
 | |
|   private async getCredentialsConfigPath(): Promise<string> {
 | |
|     if (this.credentialsConfigPath) {
 | |
|       return this.credentialsConfigPath
 | |
|     }
 | |
| 
 | |
|     const runnerTemp = process.env['RUNNER_TEMP'] || ''
 | |
|     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
 | |
| 
 | |
|     // Create a unique filename for this checkout instance
 | |
|     const configFileName = `git-credentials-${uuid()}.config`
 | |
|     this.credentialsConfigPath = path.join(runnerTemp, configFileName)
 | |
| 
 | |
|     core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
 | |
|     return this.credentialsConfigPath
 | |
|   }
 | |
| 
 | |
|   async configureTempGlobalConfig(): Promise<string> {
 | |
|     // Already setup global config
 | |
|     if (this.temporaryHomePath?.length > 0) {
 | |
|       return path.join(this.temporaryHomePath, '.gitconfig')
 | |
|     }
 | |
|     // Create a temp home directory
 | |
|     const runnerTemp = process.env['RUNNER_TEMP'] || ''
 | |
|     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
 | |
|     const uniqueId = uuid()
 | |
|     this.temporaryHomePath = path.join(runnerTemp, uniqueId)
 | |
|     await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
 | |
| 
 | |
|     // Copy the global git config
 | |
|     const gitConfigPath = path.join(
 | |
|       process.env['HOME'] || os.homedir(),
 | |
|       '.gitconfig'
 | |
|     )
 | |
|     const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
 | |
|     let configExists = false
 | |
|     try {
 | |
|       await fs.promises.stat(gitConfigPath)
 | |
|       configExists = true
 | |
|     } catch (err) {
 | |
|       if ((err as any)?.code !== 'ENOENT') {
 | |
|         throw err
 | |
|       }
 | |
|     }
 | |
|     if (configExists) {
 | |
|       core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
 | |
|       await io.cp(gitConfigPath, newGitConfigPath)
 | |
|     } else {
 | |
|       await fs.promises.writeFile(newGitConfigPath, '')
 | |
|     }
 | |
| 
 | |
|     // Override HOME
 | |
|     core.info(
 | |
|       `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
 | |
|     )
 | |
|     this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
 | |
| 
 | |
|     return newGitConfigPath
 | |
|   }
 | |
| 
 | |
|   async configureGlobalAuth(): Promise<void> {
 | |
|     // 'configureTempGlobalConfig' noops if already set, just returns the path
 | |
|     await this.configureTempGlobalConfig()
 | |
|     try {
 | |
|       // Configure the token
 | |
|       await this.configureToken(true)
 | |
| 
 | |
|       // Configure HTTPS instead of SSH
 | |
|       await this.git.tryConfigUnset(this.insteadOfKey, true)
 | |
|       if (!this.settings.sshKey) {
 | |
|         for (const insteadOfValue of this.insteadOfValues) {
 | |
|           await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
 | |
|         }
 | |
|       }
 | |
|     } catch (err) {
 | |
|       // Unset in case somehow written to the real global config
 | |
|       core.info(
 | |
|         'Encountered an error when attempting to configure token. Attempting unconfigure.'
 | |
|       )
 | |
|       await this.git.tryConfigUnset(this.tokenConfigKey, true)
 | |
|       throw err
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async configureSubmoduleAuth(): Promise<void> {
 | |
|     // Remove possible previous HTTPS instead of SSH
 | |
|     await this.removeGitConfig(this.insteadOfKey, true)
 | |
| 
 | |
|     if (this.settings.persistCredentials) {
 | |
|       // Use the same credentials config file created for the main repo
 | |
|       const credentialsConfigPath = await this.getCredentialsConfigPath()
 | |
|       const githubWorkspace = process.env['GITHUB_WORKSPACE']
 | |
|       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
 | |
| 
 | |
|       const containerCredentialsPath = path.posix.join(
 | |
|         '/github/runner_temp',
 | |
|         path.basename(credentialsConfigPath)
 | |
|       )
 | |
| 
 | |
|       // Calculate container git directory base path
 | |
|       const workingDirectory = this.git.getWorkingDirectory()
 | |
|       let relativePath = path.relative(githubWorkspace, workingDirectory)
 | |
|       relativePath = relativePath.replace(/\\/g, '/')
 | |
|       const containerWorkspaceBase = path.posix.join(
 | |
|         '/github/workspace',
 | |
|         relativePath
 | |
|       )
 | |
| 
 | |
|       // Get submodule config file paths.
 | |
|       // Use `--show-origin` to get the config file path for each submodule.
 | |
|       const output = await this.git.submoduleForeach(
 | |
|         `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
 | |
|         this.settings.nestedSubmodules
 | |
|       )
 | |
| 
 | |
|       // Extract config file paths from the output (lines starting with "file:").
 | |
|       const configPaths =
 | |
|         output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
 | |
| 
 | |
|       // For each submodule, configure includeIf entries pointing to the shared credentials file.
 | |
|       // Configure both host and container paths to support Docker container actions.
 | |
|       for (const configPath of configPaths) {
 | |
|         // Get the submodule path from its config file path.
 | |
|         const submodulePath = path.dirname(path.dirname(configPath))
 | |
|         // Configure host path includeIf.
 | |
|         // Use forward slashes for git config, even on Windows.
 | |
|         let submoduleGitDir = path.join(submodulePath, '.git')
 | |
|         submoduleGitDir = submoduleGitDir.replace(/\\/g, '/')
 | |
|         await this.git.config(
 | |
|           `includeIf.gitdir:${submoduleGitDir}.path`,
 | |
|           credentialsConfigPath,
 | |
|           false,
 | |
|           false,
 | |
|           configPath
 | |
|         )
 | |
| 
 | |
|         // Configure container path includeIf.
 | |
|         // Use forward slashes for git config, even on Windows.
 | |
|         let submoduleRelativePath = path.relative(
 | |
|           workingDirectory,
 | |
|           submodulePath
 | |
|         )
 | |
|         submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/')
 | |
|         const containerSubmoduleGitDir = path.posix.join(
 | |
|           containerWorkspaceBase,
 | |
|           submoduleRelativePath,
 | |
|           '.git'
 | |
|         )
 | |
|         await this.git.config(
 | |
|           `includeIf.gitdir:${containerSubmoduleGitDir}.path`,
 | |
|           containerCredentialsPath,
 | |
|           false,
 | |
|           false,
 | |
|           configPath
 | |
|         )
 | |
|       }
 | |
| 
 | |
|       if (this.settings.sshKey) {
 | |
|         // Configure core.sshCommand
 | |
|         await this.git.submoduleForeach(
 | |
|           `git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`,
 | |
|           this.settings.nestedSubmodules
 | |
|         )
 | |
|       } else {
 | |
|         // Configure HTTPS instead of SSH
 | |
|         for (const insteadOfValue of this.insteadOfValues) {
 | |
|           await this.git.submoduleForeach(
 | |
|             `git config --local --add '${this.insteadOfKey}' '${insteadOfValue}'`,
 | |
|             this.settings.nestedSubmodules
 | |
|           )
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async removeAuth(): Promise<void> {
 | |
|     await this.removeSsh()
 | |
|     await this.removeToken()
 | |
|   }
 | |
| 
 | |
|   async removeGlobalConfig(): Promise<void> {
 | |
|     if (this.temporaryHomePath?.length > 0) {
 | |
|       core.debug(`Unsetting HOME override`)
 | |
|       this.git.removeEnvironmentVariable('HOME')
 | |
|       await io.rmRF(this.temporaryHomePath)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async configureSsh(): Promise<void> {
 | |
|     if (!this.settings.sshKey) {
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     // Write key
 | |
|     const runnerTemp = process.env['RUNNER_TEMP'] || ''
 | |
|     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
 | |
|     const uniqueId = uuid()
 | |
|     this.sshKeyPath = path.join(runnerTemp, uniqueId)
 | |
|     stateHelper.setSshKeyPath(this.sshKeyPath)
 | |
|     await fs.promises.mkdir(runnerTemp, {recursive: true})
 | |
|     await fs.promises.writeFile(
 | |
|       this.sshKeyPath,
 | |
|       this.settings.sshKey.trim() + '\n',
 | |
|       {mode: 0o600}
 | |
|     )
 | |
| 
 | |
|     // Remove inherited permissions on Windows
 | |
|     if (IS_WINDOWS) {
 | |
|       const icacls = await io.which('icacls.exe')
 | |
|       await exec.exec(
 | |
|         `"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`
 | |
|       )
 | |
|       await exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`)
 | |
|     }
 | |
| 
 | |
|     // Write known hosts
 | |
|     const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts')
 | |
|     let userKnownHosts = ''
 | |
|     try {
 | |
|       userKnownHosts = (
 | |
|         await fs.promises.readFile(userKnownHostsPath)
 | |
|       ).toString()
 | |
|     } catch (err) {
 | |
|       if ((err as any)?.code !== 'ENOENT') {
 | |
|         throw err
 | |
|       }
 | |
|     }
 | |
|     let knownHosts = ''
 | |
|     if (userKnownHosts) {
 | |
|       knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`
 | |
|     }
 | |
|     if (this.settings.sshKnownHosts) {
 | |
|       knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
 | |
|     }
 | |
|     knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n# End implicitly added github.com\n`
 | |
|     this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
 | |
|     stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
 | |
|     await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
 | |
| 
 | |
|     // Configure GIT_SSH_COMMAND
 | |
|     const sshPath = await io.which('ssh', true)
 | |
|     this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
 | |
|       this.sshKeyPath
 | |
|     )}"`
 | |
|     if (this.settings.sshStrict) {
 | |
|       this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
 | |
|     }
 | |
|     this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
 | |
|       this.sshKnownHostsPath
 | |
|     )}"`
 | |
|     core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
 | |
|     this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
 | |
| 
 | |
|     // Configure core.sshCommand
 | |
|     if (this.settings.persistCredentials) {
 | |
|       await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async configureToken(globalConfig?: boolean): Promise<void> {
 | |
|     // Get the credentials config file path in RUNNER_TEMP
 | |
|     const credentialsConfigPath = await this.getCredentialsConfigPath()
 | |
| 
 | |
|     // Write placeholder to the separate credentials config file using git config.
 | |
|     // This approach avoids the credential being captured by process creation audit events,
 | |
|     // which are commonly logged. For more information, refer to
 | |
|     // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
 | |
|     await this.git.config(
 | |
|       this.tokenConfigKey,
 | |
|       this.tokenPlaceholderConfigValue,
 | |
|       false,
 | |
|       false,
 | |
|       credentialsConfigPath
 | |
|     )
 | |
| 
 | |
|     // Replace the placeholder in the credentials config file
 | |
|     await this.replaceTokenPlaceholder(credentialsConfigPath)
 | |
| 
 | |
|     // Add include or includeIf to reference the credentials config
 | |
|     if (globalConfig) {
 | |
|       // Global config file is temporary
 | |
|       await this.git.config('include.path', credentialsConfigPath, true)
 | |
|     } else {
 | |
|       // For local config, use includeIf.gitdir to match the .git directory.
 | |
|       // Configure for both host and container paths to support Docker container actions.
 | |
|       let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
 | |
|       // Use forward slashes for git config, even on Windows
 | |
|       gitDir = gitDir.replace(/\\/g, '/')
 | |
|       const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
 | |
|       await this.git.config(hostIncludeKey, credentialsConfigPath)
 | |
|       this.credentialsIncludeKeys.push(hostIncludeKey)
 | |
| 
 | |
|       // Configure for container scenario where paths are mapped to fixed locations
 | |
|       const githubWorkspace = process.env['GITHUB_WORKSPACE']
 | |
|       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
 | |
|       
 | |
|       // Calculate the relative path of the working directory from GITHUB_WORKSPACE
 | |
|       const workingDirectory = this.git.getWorkingDirectory()
 | |
|       let relativePath = path.relative(githubWorkspace, workingDirectory)
 | |
| 
 | |
|       // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp
 | |
|       // Use forward slashes for git config
 | |
|       relativePath = relativePath.replace(/\\/g, '/')
 | |
|       const containerGitDir = path.posix.join(
 | |
|         '/github/workspace',
 | |
|         relativePath,
 | |
|         '.git'
 | |
|       )
 | |
|       const containerCredentialsPath = path.posix.join(
 | |
|         '/github/runner_temp',
 | |
|         path.basename(credentialsConfigPath)
 | |
|       )
 | |
| 
 | |
|       const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
 | |
|       await this.git.config(containerIncludeKey, containerCredentialsPath)
 | |
|       this.credentialsIncludeKeys.push(containerIncludeKey)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async replaceTokenPlaceholder(configPath: string): Promise<void> {
 | |
|     assert.ok(configPath, 'configPath is not defined')
 | |
|     let content = (await fs.promises.readFile(configPath)).toString()
 | |
|     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
 | |
|     if (
 | |
|       placeholderIndex < 0 ||
 | |
|       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
 | |
|     ) {
 | |
|       throw new Error(`Unable to replace auth placeholder in ${configPath}`)
 | |
|     }
 | |
|     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
 | |
|     content = content.replace(
 | |
|       this.tokenPlaceholderConfigValue,
 | |
|       this.tokenConfigValue
 | |
|     )
 | |
|     await fs.promises.writeFile(configPath, content)
 | |
|   }
 | |
| 
 | |
|   private async removeSsh(): Promise<void> {
 | |
|     // SSH key
 | |
|     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
 | |
|     if (keyPath) {
 | |
|       try {
 | |
|         await io.rmRF(keyPath)
 | |
|       } catch (err) {
 | |
|         core.debug(`${(err as any)?.message ?? err}`)
 | |
|         core.warning(`Failed to remove SSH key '${keyPath}'`)
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // SSH known hosts
 | |
|     const knownHostsPath =
 | |
|       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
 | |
|     if (knownHostsPath) {
 | |
|       try {
 | |
|         await io.rmRF(knownHostsPath)
 | |
|       } catch {
 | |
|         // Intentionally empty
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // SSH command
 | |
|     await this.removeGitConfig(SSH_COMMAND_KEY)
 | |
|   }
 | |
| 
 | |
|   private async removeToken(): Promise<void> {
 | |
|     // HTTP extra header
 | |
|     await this.removeGitConfig(this.tokenConfigKey)
 | |
| 
 | |
|     // Remove include/includeIf config entries
 | |
|     for (const includeKey of this.credentialsIncludeKeys) {
 | |
|       await this.removeGitConfig(includeKey)
 | |
|     }
 | |
|     this.credentialsIncludeKeys = []
 | |
| 
 | |
|     // Remove includeIf entries from submodules
 | |
|     await this.git.submoduleForeach(
 | |
|       `sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`,
 | |
|       true
 | |
|     )
 | |
| 
 | |
|     // Remove credentials config file
 | |
|     if (this.credentialsConfigPath) {
 | |
|       try {
 | |
|         await io.rmRF(this.credentialsConfigPath)
 | |
|       } catch (err) {
 | |
|         core.debug(`${(err as any)?.message ?? err}`)
 | |
|         core.warning(
 | |
|           `Failed to remove credentials config '${this.credentialsConfigPath}'`
 | |
|         )
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async removeGitConfig(
 | |
|     configKey: string,
 | |
|     submoduleOnly: boolean = false
 | |
|   ): Promise<void> {
 | |
|     if (!submoduleOnly) {
 | |
|       if (
 | |
|         (await this.git.configExists(configKey)) &&
 | |
|         !(await this.git.tryConfigUnset(configKey))
 | |
|       ) {
 | |
|         // Load the config contents
 | |
|         core.warning(`Failed to remove '${configKey}' from the git config`)
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const pattern = regexpHelper.escape(configKey)
 | |
|     await this.git.submoduleForeach(
 | |
|       // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
 | |
|       `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
 | |
|       true
 | |
|     )
 | |
|   }
 | |
| }
 | 
