mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 13:58:09 +08:00 
			
		
		
		
	Persist creds to a separate file
This commit is contained in:
		| @@ -43,6 +43,8 @@ class GitAuthHelper { | ||||
|   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, | ||||
| @@ -81,6 +83,22 @@ class GitAuthHelper { | ||||
|     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) { | ||||
| @@ -126,10 +144,10 @@ class GitAuthHelper { | ||||
|  | ||||
|   async configureGlobalAuth(): Promise<void> { | ||||
|     // 'configureTempGlobalConfig' noops if already set, just returns the path | ||||
|     const newGitConfigPath = await this.configureTempGlobalConfig() | ||||
|     await this.configureTempGlobalConfig() | ||||
|     try { | ||||
|       // Configure the token | ||||
|       await this.configureToken(newGitConfigPath, true) | ||||
|       await this.configureToken(true) | ||||
|  | ||||
|       // Configure HTTPS instead of SSH | ||||
|       await this.git.tryConfigUnset(this.insteadOfKey, true) | ||||
| @@ -272,32 +290,62 @@ class GitAuthHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async configureToken( | ||||
|     configPath?: string, | ||||
|     globalConfig?: boolean | ||||
|   ): Promise<void> { | ||||
|     // Validate args | ||||
|     assert.ok( | ||||
|       (configPath && globalConfig) || (!configPath && !globalConfig), | ||||
|       'Unexpected configureToken parameter combinations' | ||||
|     ) | ||||
|   private async configureToken(globalConfig?: boolean): Promise<void> { | ||||
|     // Get the credentials config file path in RUNNER_TEMP | ||||
|     const credentialsConfigPath = await this.getCredentialsConfigPath() | ||||
|  | ||||
|     // Default config path | ||||
|     if (!configPath && !globalConfig) { | ||||
|       configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config') | ||||
|     } | ||||
|  | ||||
|     // Configure a placeholder value. 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 | ||||
|     // 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, | ||||
|       globalConfig | ||||
|       false, | ||||
|       false, | ||||
|       credentialsConfigPath | ||||
|     ) | ||||
|  | ||||
|     // Replace the placeholder | ||||
|     await this.replaceTokenPlaceholder(configPath || '') | ||||
|     // Replace the placeholder in the credentials config file | ||||
|     await this.replaceTokenPlaceholder(credentialsConfigPath) | ||||
|  | ||||
|     // Add include or includeIf to reference the credentials config | ||||
|     if (globalConfig) { | ||||
|       // For global config, use unconditional include. | ||||
|       // No need to track for cleanup since the temp .gitconfig file (which contains | ||||
|       // this include.path entry) gets deleted by removeGlobalConfig(). | ||||
|       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. | ||||
|       const gitDir = path.join(this.git.getWorkingDirectory(), '.git') | ||||
|       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'] | ||||
|       if (githubWorkspace) { | ||||
|         // Calculate the relative path of the working directory from GITHUB_WORKSPACE | ||||
|         const workingDirectory = this.git.getWorkingDirectory() | ||||
|         const relativePath = path.relative(githubWorkspace, workingDirectory) | ||||
|  | ||||
|         // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp | ||||
|         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> { | ||||
| @@ -348,6 +396,24 @@ class GitAuthHelper { | ||||
|   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 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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user