pull-merge icon indicating copy to clipboard operation
pull-merge copied to clipboard

chore(deps): update actions/checkout action to v5.0.1

Open renovate[bot] opened this issue 1 month ago • 3 comments

This PR contains the following updates:

Package Type Update Change
actions/checkout action patch v5.0.0 -> v5.0.1

Release Notes

actions/checkout (actions/checkout)

v5.0.1

Compare Source

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v5...v5.0.1


Configuration

📅 Schedule: Branch creation - Tuesday through Thursday ( * * * * 2-4 ) (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • [ ] If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

renovate[bot] avatar Nov 25 '25 01:11 renovate[bot]

openai debug - [puLL-Merge] - actions/[email protected]

Diff

diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..9acba5463 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -675,6 +675,283 @@ describe('git-auth-helper tests', () => {
     expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
   })
 
+  const removeAuth_removesV6StyleCredentials =
+    'removeAuth removes v6 style credentials'
+  it(removeAuth_removesV6StyleCredentials, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentials)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Manually create v6-style credentials that would be left by v6
+    const credentialsFileName =
+      'git-credentials-12345678-1234-1234-1234-123456789abc.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to local git config (simulating v6 configuration)
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
+    )
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Mock the git methods to handle v6 cleanup
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockResolvedValue([
+      `includeIf.gitdir:${hostGitDir}/.path`,
+      'includeIf.gitdir:/github/workspace/.git/.path'
+    ])
+
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(async (key: string) => {
+      if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
+        return [credentialsFilePath]
+      }
+      if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
+        return [`/github/runner_temp/${credentialsFileName}`]
+      }
+      return []
+    })
+
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        // Remove the includeIf section
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert includeIf entries removed from local git config
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(gitConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesV6StyleCredentialsFromSubmodules =
+    'removeAuth removes v6 style credentials from submodules'
+  it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file
+    const credentialsFileName =
+      'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to submodule config
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      submodule1ConfigPath,
+      `[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+
+    // Verify submodule config has includeIf entry
+    let submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
+      0
+    )
+    expect(
+      submoduleConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Mock getSubmoduleConfigPaths
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
+
+    // Mock tryGetConfigKeys for submodule
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockImplementation(
+      async (pattern: string, globalConfig?: boolean, configPath?: string) => {
+        if (configPath === submodule1ConfigPath) {
+          return [`includeIf.gitdir:${submodule1GitDir}/.path`]
+        }
+        return []
+      }
+    )
+
+    // Mock tryGetConfigValues for submodule
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(
+      async (key: string, globalConfig?: boolean, configPath?: string) => {
+        if (
+          configPath === submodule1ConfigPath &&
+          key === `includeIf.gitdir:${submodule1GitDir}/.path`
+        ) {
+          return [credentialsFilePath]
+        }
+        return []
+      }
+    )
+
+    // Mock tryConfigUnsetValue for submodule
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert submodule includeIf entries removed
+    submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_skipsV6CleanupWhenEnvVarSet =
+    'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
+  it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
+    // Arrange
+    await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
+
+    // Set the skip environment variable
+    process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file in RUNNER_TEMP
+    const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const credentialsContent =
+      '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf section to local git config (separate from http.* config)
+    const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
+    await fs.promises.appendFile(localGitConfigPath, includeIfSection)
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert v5 cleanup still happened (http.* removed)
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(
+      gitConfigContent.indexOf('http.https://github.com/.extraheader')
+    ).toBeLessThan(0)
+
+    // Assert v6 cleanup was skipped - includeIf should still be present
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials file still exists (wasn't deleted)
+    await fs.promises.stat(credentialsFilePath) // File should still exist
+
+    // Assert debug message was logged
+    expect(core.debug).toHaveBeenCalledWith(
+      'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+    )
+
+    // Cleanup
+    delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+  })
+
   const removeGlobalConfig_removesOverride =
     'removeGlobalConfig removes override'
   it(removeGlobalConfig_removesOverride, async () => {
@@ -796,6 +1073,18 @@ async function setup(testName: string): Promise<void> {
     ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(),
     version: jest.fn()
   }
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..1627b842e 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -499,6 +499,18 @@ async function setup(testName: string): Promise<void> {
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git dist/index.js dist/index.js
index f3ae6f3ea..b0add8ac4 100644
--- dist/index.js
+++ dist/index.js
@@ -411,8 +411,50 @@ class GitAuthHelper {
     }
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            // Remove HTTP extra header from local git config and submodule configs
             yield this.removeGitConfig(this.tokenConfigKey);
+            //
+            // Cleanup actions/checkout@v6 style credentials
+            //
+            const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'];
+            if (skipV6Cleanup === '1' || (skipV6Cleanup === null || skipV6Cleanup === void 0 ? void 0 : skipV6Cleanup.toLowerCase()) === 'true') {
+                core.debug('Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP');
+                return;
+            }
+            try {
+                // Collect credentials config paths that need to be removed
+                const credentialsPaths = new Set();
+                // Remove includeIf entries that point to git-credentials-*.config files
+                const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+                mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                // Remove submodule includeIf entries that point to git-credentials-*.config files
+                try {
+                    const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+                    for (const configPath of submoduleConfigPaths) {
+                        const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                        submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                    }
+                }
+                catch (err) {
+                    core.debug(`Unable to get submodule config paths: ${err}`);
+                }
+                // Remove credentials config files
+                for (const credentialsPath of credentialsPaths) {
+                    // Only remove credentials config files if they are under RUNNER_TEMP
+                    const runnerTemp = process.env['RUNNER_TEMP'];
+                    if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+                        try {
+                            yield io.rmRF(credentialsPath);
+                        }
+                        catch (err) {
+                            core.debug(`Failed to remove credentials config '${credentialsPath}': ${err}`);
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                core.debug(`Failed to cleanup v6 style credentials: ${err}`);
+            }
         });
     }
     removeGitConfig(configKey_1) {
@@ -430,6 +472,49 @@ class GitAuthHelper {
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * This handles cleanup of credentials configured by newer versions of the action.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                core.debug(`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`);
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -706,6 +791,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +931,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +964,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..0c82dddab 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -346,8 +346,58 @@ class GitAuthHelper {
   }
 
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header from local git config and submodule configs
     await this.removeGitConfig(this.tokenConfigKey)
+
+    //
+    // Cleanup actions/checkout@v6 style credentials
+    //
+    const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+    if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
+      core.debug(
+        'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+      )
+      return
+    }
+
+    try {
+      // Collect credentials config paths that need to be removed
+      const credentialsPaths = new Set<string>()
+
+      // Remove includeIf entries that point to git-credentials-*.config files
+      const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+      mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+      // Remove submodule includeIf entries that point to git-credentials-*.config files
+      try {
+        const submoduleConfigPaths =
+          await this.git.getSubmoduleConfigPaths(true)
+        for (const configPath of submoduleConfigPaths) {
+          const submoduleCredentialsPaths =
+            await this.removeIncludeIfCredentials(configPath)
+          submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+        }
+      } catch (err) {
+        core.debug(`Unable to get submodule config paths: ${err}`)
+      }
+
+      // Remove credentials config files
+      for (const credentialsPath of credentialsPaths) {
+        // Only remove credentials config files if they are under RUNNER_TEMP
+        const runnerTemp = process.env['RUNNER_TEMP']
+        if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+          try {
+            await io.rmRF(credentialsPath)
+          } catch (err) {
+            core.debug(
+              `Failed to remove credentials config '${credentialsPath}': ${err}`
+            )
+          }
+        }
+      }
+    } catch (err) {
+      core.debug(`Failed to cleanup v6 style credentials: ${err}`)
+    }
   }
 
   private async removeGitConfig(
@@ -371,4 +421,59 @@ class GitAuthHelper {
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * This handles cleanup of credentials configured by newer versions of the action.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      core.debug(
+        `Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
+      )
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..9c789ac80 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -41,6 +41,7 @@ export interface IGitCommandManager {
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +60,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -323,6 +340,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +487,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +531,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0


Description

This pull request adds functionality to handle the cleanup of v6-style Git credentials after the actions/checkout@v6 execution. This involves removing entries related to includeIf git configurations and the associated credential files. The main changes are in:

  • Introducing new helper functions in the GitAuthHelper class to identify and remove these configuration entries and their associated files.
  • Expanding the GitCommandManager to support additional Git operations related to this cleanup.
  • Adding unit tests to ensure the new functionality works as intended.

Possible Issues

  1. Compatibility Concerns: Since v6 configuration cleanup involves specific file patterns and environment configurations, there might be edge cases where this logic does not perform as expected if the environment deviates from expected patterns.
  2. Mock Behavior in Tests: The added unit tests rely heavily on mocked behavior for file system operations and git commands. If the mock implementations don't align perfectly with actual behavior, some edge cases may not be adequately covered.
  3. Environment Variable Control: The skipping mechanism is controlled by an environment variable (ACTIONS_CHECKOUT_SKIP_V6_CLEANUP). If not set or incorrectly set, it might affect the intended cleanup behavior.

Security Hotspots

  1. Unauthorized File Deletion: The cleanup process involves deleting credential files, which could lead to unintentional file deletions if paths are incorrectly identified. There is some mitigation since the deletion is restricted to RUNNER_TEMP directory, but caution is still necessary.
  2. Pattern Matching for Credentials: The cleanup relies on identifying credential files with a specific naming pattern. If an attacker could place files matching this pattern outside of allowed directories, it might lead to disruptive deletions.

Privacy Hotspots

  1. Handling of Auth Tokens: While the authenticated access tokens seem to be temporary and created specifically for the git operations, ensuring these tokens are not leaked in logs or retained longer than necessary is essential.
Changes

Changes

  1. Unit Tests (__test__/git-auth-helper.test.ts and __test__/git-directory-helper.test.ts):

    • Added tests to verify the cleanup of v6-style credentials.
    • Mocks added to simulate git commands and file system interactions.
  2. Git Auth Helper (src/git-auth-helper.ts, dist/index.js):

    • Updated to include logic for removing v6-style credentials.
    • Added removeIncludeIfCredentials and testCredentialsConfigPath methods.
    • Expanded the removeToken method to include new cleanup logic.
  3. Git Command Manager (src/git-command-manager.ts, dist/index.js):

    • Added methods to get configuration keys and values, unset specific configuration values.
    • Introduced logic to obtain submodule configuration paths.
sequenceDiagram
    participant User
    participant GitAuthHelper
    participant GitCommandManager
    participant FS as FileSystem
    participant Core
    participant IO 

    User->>GitAuthHelper: Call removeAuth()
    GitAuthHelper->>GitAuthHelper: Check ACTIONS_CHECKOUT_SKIP_V6_CLEANUP
    alt Cleanup Skipped
        GitAuthHelper->>Core: Log "Skipping v6 style cleanup"
    else Normal Cleanup
        GitAuthHelper->>GitAuthHelper: Call removeGitConfig()
        GitAuthHelper->>GitAuthHelper: Call removeIncludeIfCredentials()
        GitAuthHelper->>GitCommandManager: getSubmoduleConfigPaths()
        GitCommandManager->>GitAuthHelper: Return submodule config paths
        
        loop Remove Submodule Configs
            GitAuthHelper->>GitAuthHelper: Call removeIncludeIfCredentials(config path)
        end
        
        loop Remove Credential Files
            GitAuthHelper->>IO: Call rmRF(credentialsPath)
        end
        GitAuthHelper->>Core: Log Debug Info
    end
    return

github-actions[bot] avatar Nov 25 '25 01:11 github-actions[bot]

bedrock debug - [puLL-Merge] - actions/[email protected]

Diff

diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..9acba5463 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -675,6 +675,283 @@ describe('git-auth-helper tests', () => {
     expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
   })
 
+  const removeAuth_removesV6StyleCredentials =
+    'removeAuth removes v6 style credentials'
+  it(removeAuth_removesV6StyleCredentials, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentials)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Manually create v6-style credentials that would be left by v6
+    const credentialsFileName =
+      'git-credentials-12345678-1234-1234-1234-123456789abc.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to local git config (simulating v6 configuration)
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
+    )
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Mock the git methods to handle v6 cleanup
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockResolvedValue([
+      `includeIf.gitdir:${hostGitDir}/.path`,
+      'includeIf.gitdir:/github/workspace/.git/.path'
+    ])
+
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(async (key: string) => {
+      if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
+        return [credentialsFilePath]
+      }
+      if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
+        return [`/github/runner_temp/${credentialsFileName}`]
+      }
+      return []
+    })
+
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        // Remove the includeIf section
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert includeIf entries removed from local git config
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(gitConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesV6StyleCredentialsFromSubmodules =
+    'removeAuth removes v6 style credentials from submodules'
+  it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file
+    const credentialsFileName =
+      'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to submodule config
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      submodule1ConfigPath,
+      `[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+
+    // Verify submodule config has includeIf entry
+    let submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
+      0
+    )
+    expect(
+      submoduleConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Mock getSubmoduleConfigPaths
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
+
+    // Mock tryGetConfigKeys for submodule
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockImplementation(
+      async (pattern: string, globalConfig?: boolean, configPath?: string) => {
+        if (configPath === submodule1ConfigPath) {
+          return [`includeIf.gitdir:${submodule1GitDir}/.path`]
+        }
+        return []
+      }
+    )
+
+    // Mock tryGetConfigValues for submodule
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(
+      async (key: string, globalConfig?: boolean, configPath?: string) => {
+        if (
+          configPath === submodule1ConfigPath &&
+          key === `includeIf.gitdir:${submodule1GitDir}/.path`
+        ) {
+          return [credentialsFilePath]
+        }
+        return []
+      }
+    )
+
+    // Mock tryConfigUnsetValue for submodule
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert submodule includeIf entries removed
+    submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_skipsV6CleanupWhenEnvVarSet =
+    'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
+  it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
+    // Arrange
+    await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
+
+    // Set the skip environment variable
+    process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file in RUNNER_TEMP
+    const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const credentialsContent =
+      '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf section to local git config (separate from http.* config)
+    const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
+    await fs.promises.appendFile(localGitConfigPath, includeIfSection)
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert v5 cleanup still happened (http.* removed)
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(
+      gitConfigContent.indexOf('http.https://github.com/.extraheader')
+    ).toBeLessThan(0)
+
+    // Assert v6 cleanup was skipped - includeIf should still be present
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials file still exists (wasn't deleted)
+    await fs.promises.stat(credentialsFilePath) // File should still exist
+
+    // Assert debug message was logged
+    expect(core.debug).toHaveBeenCalledWith(
+      'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+    )
+
+    // Cleanup
+    delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+  })
+
   const removeGlobalConfig_removesOverride =
     'removeGlobalConfig removes override'
   it(removeGlobalConfig_removesOverride, async () => {
@@ -796,6 +1073,18 @@ async function setup(testName: string): Promise<void> {
     ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(),
     version: jest.fn()
   }
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..1627b842e 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -499,6 +499,18 @@ async function setup(testName: string): Promise<void> {
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git dist/index.js dist/index.js
index f3ae6f3ea..b0add8ac4 100644
--- dist/index.js
+++ dist/index.js
@@ -411,8 +411,50 @@ class GitAuthHelper {
     }
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            // Remove HTTP extra header from local git config and submodule configs
             yield this.removeGitConfig(this.tokenConfigKey);
+            //
+            // Cleanup actions/checkout@v6 style credentials
+            //
+            const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'];
+            if (skipV6Cleanup === '1' || (skipV6Cleanup === null || skipV6Cleanup === void 0 ? void 0 : skipV6Cleanup.toLowerCase()) === 'true') {
+                core.debug('Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP');
+                return;
+            }
+            try {
+                // Collect credentials config paths that need to be removed
+                const credentialsPaths = new Set();
+                // Remove includeIf entries that point to git-credentials-*.config files
+                const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+                mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                // Remove submodule includeIf entries that point to git-credentials-*.config files
+                try {
+                    const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+                    for (const configPath of submoduleConfigPaths) {
+                        const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                        submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                    }
+                }
+                catch (err) {
+                    core.debug(`Unable to get submodule config paths: ${err}`);
+                }
+                // Remove credentials config files
+                for (const credentialsPath of credentialsPaths) {
+                    // Only remove credentials config files if they are under RUNNER_TEMP
+                    const runnerTemp = process.env['RUNNER_TEMP'];
+                    if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+                        try {
+                            yield io.rmRF(credentialsPath);
+                        }
+                        catch (err) {
+                            core.debug(`Failed to remove credentials config '${credentialsPath}': ${err}`);
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                core.debug(`Failed to cleanup v6 style credentials: ${err}`);
+            }
         });
     }
     removeGitConfig(configKey_1) {
@@ -430,6 +472,49 @@ class GitAuthHelper {
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * This handles cleanup of credentials configured by newer versions of the action.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                core.debug(`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`);
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -706,6 +791,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +931,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +964,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..0c82dddab 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -346,8 +346,58 @@ class GitAuthHelper {
   }
 
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header from local git config and submodule configs
     await this.removeGitConfig(this.tokenConfigKey)
+
+    //
+    // Cleanup actions/checkout@v6 style credentials
+    //
+    const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+    if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
+      core.debug(
+        'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+      )
+      return
+    }
+
+    try {
+      // Collect credentials config paths that need to be removed
+      const credentialsPaths = new Set<string>()
+
+      // Remove includeIf entries that point to git-credentials-*.config files
+      const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+      mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+      // Remove submodule includeIf entries that point to git-credentials-*.config files
+      try {
+        const submoduleConfigPaths =
+          await this.git.getSubmoduleConfigPaths(true)
+        for (const configPath of submoduleConfigPaths) {
+          const submoduleCredentialsPaths =
+            await this.removeIncludeIfCredentials(configPath)
+          submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+        }
+      } catch (err) {
+        core.debug(`Unable to get submodule config paths: ${err}`)
+      }
+
+      // Remove credentials config files
+      for (const credentialsPath of credentialsPaths) {
+        // Only remove credentials config files if they are under RUNNER_TEMP
+        const runnerTemp = process.env['RUNNER_TEMP']
+        if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+          try {
+            await io.rmRF(credentialsPath)
+          } catch (err) {
+            core.debug(
+              `Failed to remove credentials config '${credentialsPath}': ${err}`
+            )
+          }
+        }
+      }
+    } catch (err) {
+      core.debug(`Failed to cleanup v6 style credentials: ${err}`)
+    }
   }
 
   private async removeGitConfig(
@@ -371,4 +421,59 @@ class GitAuthHelper {
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * This handles cleanup of credentials configured by newer versions of the action.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      core.debug(
+        `Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
+      )
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..9c789ac80 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -41,6 +41,7 @@ export interface IGitCommandManager {
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +60,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -323,6 +340,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +487,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +531,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0


Description

This PR adds functionality to properly clean up credentials when using the checkout action. Specifically, it adds support for cleaning up credentials configured by newer versions (v6+) of the action which use a different credential storage mechanism. The changes ensure that authentication tokens are properly removed after checkout operations, avoiding potential security issues.

Security Hotspots

  1. Credential File Cleanup: The PR addresses a security issue where v6-style credential files might be left behind after checkout operations complete. Proper cleanup of these credential files is essential as they contain authentication tokens.
  2. Token Handling: There is some token handling in removeAuth() that now deletes credential files more thoroughly. Proper token cleanup is essential for security.
Changes

Changes

  1. src/git-auth-helper.ts

    • Enhanced the removeToken() method to clean up v6-style credentials
    • Added the removeIncludeIfCredentials() method to handle cleanup of includeIf entries in git config
    • Added testCredentialsConfigPath() to identify credential config files
  2. src/git-command-manager.ts

    • Added several new git command utilities:
      • getSubmoduleConfigPaths() - retrieves paths to submodule config files
      • tryConfigUnsetValue() - removes specific values from git config
      • tryGetConfigValues() - retrieves values for a specific config key
      • tryGetConfigKeys() - retrieves config keys matching a pattern
  3. test files

    • Added extensive tests for the new functionality in __test__/git-auth-helper.test.ts
    • Added test mocks in __test__/git-directory-helper.test.ts
    • Test cases cover:
      • Removal of v6-style credentials
      • Handling of submodule credentials
      • Skip functionality via environment variable
  4. dist/index.js

    • Compiled changes from the TypeScript source files
sequenceDiagram
    participant App as Checkout Action
    participant Auth as GitAuthHelper
    participant Git as GitCommandManager
    participant Config as Git Config
    participant Filesystem

    App->>Auth: removeAuth()
    Auth->>Git: removeGitConfig(tokenConfigKey)
    Git-->>Config: Remove HTTP extra header (v5 style)
    
    Note over Auth: Begin v6 style cleanup
    
    Auth->>Auth: Check ACTIONS_CHECKOUT_SKIP_V6_CLEANUP
    
    opt When v6 cleanup is not skipped
        Auth->>Auth: removeIncludeIfCredentials()
        Auth->>Git: tryGetConfigKeys('^includeIf\\.gitdir:')
        Git-->>Auth: includeIf keys
        
        loop For each includeIf key
            Auth->>Git: tryGetConfigValues(key)
            Git-->>Auth: config values
            
            loop For each value matching credentials pattern
                Auth->>Git: tryConfigUnsetValue(key, value)
                Git-->>Config: Remove includeIf entry
                Auth->>Filesystem: Track credential path
            end
        end
        
        Auth->>Git: getSubmoduleConfigPaths()
        Git-->>Auth: submodule config paths
        
        loop For each submodule config
            Auth->>Auth: removeIncludeIfCredentials(configPath)
            Note over Auth,Config: Same process as above but for submodule configs
        end
        
        loop For each credential file path
            Auth->>Filesystem: io.rmRF(credentialsPath)
            Filesystem-->>Auth: File deleted
        end
    end

github-actions[bot] avatar Nov 25 '25 01:11 github-actions[bot]

anthropic debug - [puLL-Merge] - actions/[email protected]

Diff

diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..9acba5463 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -675,6 +675,283 @@ describe('git-auth-helper tests', () => {
     expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
   })
 
+  const removeAuth_removesV6StyleCredentials =
+    'removeAuth removes v6 style credentials'
+  it(removeAuth_removesV6StyleCredentials, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentials)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Manually create v6-style credentials that would be left by v6
+    const credentialsFileName =
+      'git-credentials-12345678-1234-1234-1234-123456789abc.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to local git config (simulating v6 configuration)
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+    await fs.promises.appendFile(
+      localGitConfigPath,
+      `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
+    )
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Mock the git methods to handle v6 cleanup
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockResolvedValue([
+      `includeIf.gitdir:${hostGitDir}/.path`,
+      'includeIf.gitdir:/github/workspace/.git/.path'
+    ])
+
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(async (key: string) => {
+      if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
+        return [credentialsFilePath]
+      }
+      if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
+        return [`/github/runner_temp/${credentialsFileName}`]
+      }
+      return []
+    })
+
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        // Remove the includeIf section
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert includeIf entries removed from local git config
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(gitConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesV6StyleCredentialsFromSubmodules =
+    'removeAuth removes v6 style credentials from submodules'
+  it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file
+    const credentialsFileName =
+      'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf entries to submodule config
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    await fs.promises.appendFile(
+      submodule1ConfigPath,
+      `[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
+    )
+
+    // Verify submodule config has includeIf entry
+    let submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
+      0
+    )
+    expect(
+      submoduleConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Mock getSubmoduleConfigPaths
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
+
+    // Mock tryGetConfigKeys for submodule
+    const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
+    mockTryGetConfigKeys.mockImplementation(
+      async (pattern: string, globalConfig?: boolean, configPath?: string) => {
+        if (configPath === submodule1ConfigPath) {
+          return [`includeIf.gitdir:${submodule1GitDir}/.path`]
+        }
+        return []
+      }
+    )
+
+    // Mock tryGetConfigValues for submodule
+    const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
+    mockTryGetConfigValues.mockImplementation(
+      async (key: string, globalConfig?: boolean, configPath?: string) => {
+        if (
+          configPath === submodule1ConfigPath &&
+          key === `includeIf.gitdir:${submodule1GitDir}/.path`
+        ) {
+          return [credentialsFilePath]
+        }
+        return []
+      }
+    )
+
+    // Mock tryConfigUnsetValue for submodule
+    const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
+      any,
+      any
+    >
+    mockTryConfigUnsetValue.mockImplementation(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ) => {
+        const targetPath = configPath || localGitConfigPath
+        let content = await fs.promises.readFile(targetPath, 'utf8')
+        const lines = content
+          .split('\n')
+          .filter(line => !line.includes('includeIf') && !line.includes(value))
+        await fs.promises.writeFile(targetPath, lines.join('\n'))
+        return true
+      }
+    )
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert submodule includeIf entries removed
+    submoduleConfigContent = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
+    expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+
+    // Assert credentials file deleted
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_skipsV6CleanupWhenEnvVarSet =
+    'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
+  it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
+    // Arrange
+    await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
+
+    // Set the skip environment variable
+    process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+
+    // Create v6-style credentials file in RUNNER_TEMP
+    const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
+    const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
+    const credentialsContent =
+      '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
+    await fs.promises.writeFile(credentialsFilePath, credentialsContent)
+
+    // Add includeIf section to local git config (separate from http.* config)
+    const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
+    await fs.promises.appendFile(localGitConfigPath, includeIfSection)
+
+    // Verify v6 style config exists
+    let gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    await fs.promises.stat(credentialsFilePath) // Verify file exists
+
+    // Act
+    await authHelper.removeAuth()
+
+    // Assert v5 cleanup still happened (http.* removed)
+    gitConfigContent = (
+      await fs.promises.readFile(localGitConfigPath)
+    ).toString()
+    expect(
+      gitConfigContent.indexOf('http.https://github.com/.extraheader')
+    ).toBeLessThan(0)
+
+    // Assert v6 cleanup was skipped - includeIf should still be present
+    expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
+    expect(
+      gitConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials file still exists (wasn't deleted)
+    await fs.promises.stat(credentialsFilePath) // File should still exist
+
+    // Assert debug message was logged
+    expect(core.debug).toHaveBeenCalledWith(
+      'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+    )
+
+    // Cleanup
+    delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+  })
+
   const removeGlobalConfig_removesOverride =
     'removeGlobalConfig removes override'
   it(removeGlobalConfig_removesOverride, async () => {
@@ -796,6 +1073,18 @@ async function setup(testName: string): Promise<void> {
     ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(),
     version: jest.fn()
   }
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..1627b842e 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -499,6 +499,18 @@ async function setup(testName: string): Promise<void> {
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    getSubmoduleConfigPaths: jest.fn(async () => {
+      return []
+    }),
+    tryConfigUnsetValue: jest.fn(async () => {
+      return true
+    }),
+    tryGetConfigValues: jest.fn(async () => {
+      return []
+    }),
+    tryGetConfigKeys: jest.fn(async () => {
+      return []
+    }),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git dist/index.js dist/index.js
index f3ae6f3ea..b0add8ac4 100644
--- dist/index.js
+++ dist/index.js
@@ -411,8 +411,50 @@ class GitAuthHelper {
     }
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            // Remove HTTP extra header from local git config and submodule configs
             yield this.removeGitConfig(this.tokenConfigKey);
+            //
+            // Cleanup actions/checkout@v6 style credentials
+            //
+            const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'];
+            if (skipV6Cleanup === '1' || (skipV6Cleanup === null || skipV6Cleanup === void 0 ? void 0 : skipV6Cleanup.toLowerCase()) === 'true') {
+                core.debug('Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP');
+                return;
+            }
+            try {
+                // Collect credentials config paths that need to be removed
+                const credentialsPaths = new Set();
+                // Remove includeIf entries that point to git-credentials-*.config files
+                const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+                mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                // Remove submodule includeIf entries that point to git-credentials-*.config files
+                try {
+                    const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+                    for (const configPath of submoduleConfigPaths) {
+                        const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                        submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+                    }
+                }
+                catch (err) {
+                    core.debug(`Unable to get submodule config paths: ${err}`);
+                }
+                // Remove credentials config files
+                for (const credentialsPath of credentialsPaths) {
+                    // Only remove credentials config files if they are under RUNNER_TEMP
+                    const runnerTemp = process.env['RUNNER_TEMP'];
+                    if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+                        try {
+                            yield io.rmRF(credentialsPath);
+                        }
+                        catch (err) {
+                            core.debug(`Failed to remove credentials config '${credentialsPath}': ${err}`);
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                core.debug(`Failed to cleanup v6 style credentials: ${err}`);
+            }
         });
     }
     removeGitConfig(configKey_1) {
@@ -430,6 +472,49 @@ class GitAuthHelper {
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * This handles cleanup of credentials configured by newer versions of the action.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                core.debug(`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`);
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -706,6 +791,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +931,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +964,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..0c82dddab 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -346,8 +346,58 @@ class GitAuthHelper {
   }
 
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header from local git config and submodule configs
     await this.removeGitConfig(this.tokenConfigKey)
+
+    //
+    // Cleanup actions/checkout@v6 style credentials
+    //
+    const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
+    if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
+      core.debug(
+        'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
+      )
+      return
+    }
+
+    try {
+      // Collect credentials config paths that need to be removed
+      const credentialsPaths = new Set<string>()
+
+      // Remove includeIf entries that point to git-credentials-*.config files
+      const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+      mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+      // Remove submodule includeIf entries that point to git-credentials-*.config files
+      try {
+        const submoduleConfigPaths =
+          await this.git.getSubmoduleConfigPaths(true)
+        for (const configPath of submoduleConfigPaths) {
+          const submoduleCredentialsPaths =
+            await this.removeIncludeIfCredentials(configPath)
+          submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+        }
+      } catch (err) {
+        core.debug(`Unable to get submodule config paths: ${err}`)
+      }
+
+      // Remove credentials config files
+      for (const credentialsPath of credentialsPaths) {
+        // Only remove credentials config files if they are under RUNNER_TEMP
+        const runnerTemp = process.env['RUNNER_TEMP']
+        if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
+          try {
+            await io.rmRF(credentialsPath)
+          } catch (err) {
+            core.debug(
+              `Failed to remove credentials config '${credentialsPath}': ${err}`
+            )
+          }
+        }
+      }
+    } catch (err) {
+      core.debug(`Failed to cleanup v6 style credentials: ${err}`)
+    }
   }
 
   private async removeGitConfig(
@@ -371,4 +421,59 @@ class GitAuthHelper {
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * This handles cleanup of credentials configured by newer versions of the action.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      core.debug(
+        `Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
+      )
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern used by newer versions.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..9c789ac80 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -41,6 +41,7 @@ export interface IGitCommandManager {
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +60,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -323,6 +340,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +487,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +531,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0


Description

This PR adds cleanup logic for v6-style credentials left behind by the actions/checkout@v6 action. The v6 action used a different authentication mechanism that stored credentials in separate config files (with pattern git-credentials-<uuid>.config) and referenced them via includeIf directives in git config. This PR ensures these credentials are properly cleaned up when removeAuth() is called, including credentials in submodules. An escape hatch is provided via the ACTIONS_CHECKOUT_SKIP_V6_CLEANUP environment variable.

Possible Issues

  1. Pattern matching may be too broad: The regex /git-credentials-[0-9a-f-]+\.config$/i could potentially match legitimate user files if they follow a similar naming pattern (though unlikely given the specific format).

  2. Silent failure on permission errors: The code uses core.debug() for errors during cleanup, which means permission errors or other issues won't be visible unless debug logging is enabled. This could leave credentials behind without clear indication to users.

  3. Race condition potential: If multiple cleanup operations run concurrently (e.g., in parallel jobs), they could interfere with each other when removing shared credential files.

  4. No verification of cleanup success: The code doesn't verify that all credentials were actually removed before completing, which could leave partial cleanup state.

Security Hotspots

  1. Path traversal risk in credential file deletion (Medium Risk)

    • Location: src/git-auth-helper.ts:385-390
    • The code checks if credentialsPath.startsWith(runnerTemp) to prevent deleting files outside RUNNER_TEMP, but startsWith() is vulnerable to path traversal. An attacker could potentially craft a path like /tmp/runner/../../../etc/passwd that might pass the check depending on how runnerTemp is set.
    • Recommendation: Use path.resolve() and path.relative() to ensure the resolved path is actually within RUNNER_TEMP:
    const resolvedPath = path.resolve(credentialsPath)
    const relativePath = path.relative(runnerTemp, resolvedPath)
    if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))
    
  2. Credential file contents not validated before deletion (Low Risk)

    • Location: src/git-auth-helper.ts:385-396
    • The code deletes files matching the pattern without verifying their contents are actually credential files. While protected by the RUNNER_TEMP check, a malicious actor with access to create files in RUNNER_TEMP could potentially cause unintended file deletion.
  3. Environment variable trust (Low Risk)

    • Location: src/git-auth-helper.ts:355-361
    • The ACTIONS_CHECKOUT_SKIP_V6_CLEANUP environment variable can be set to bypass cleanup entirely, potentially leaving credentials on the runner. While this is an intentional escape hatch, it could be abused if an attacker can control environment variables.

Privacy Hotspots

  1. Debug logging of config paths (Low Risk)
    • Location: src/git-auth-helper.ts:388-390, src/git-auth-helper.ts:335
    • Error messages log full file paths which may contain sensitive information about the runner's directory structure or repository paths. While only visible with debug logging enabled, this could leak information in CI/CD logs that are publicly visible.
Changes

Changes

src/git-auth-helper.ts:

  • Added removeIncludeIfCredentials() method to find and remove includeIf directives pointing to credential config files
  • Added testCredentialsConfigPath() helper to validate credential file path patterns
  • Modified removeToken() to perform v6-style cleanup after removing HTTP headers
  • Added logic to process both main repository and submodule configs
  • Added environment variable check ACTIONS_CHECKOUT_SKIP_V6_CLEANUP to allow skipping v6 cleanup
  • Added path validation to ensure only files under RUNNER_TEMP are deleted

src/git-command-manager.ts:

  • Added getSubmoduleConfigPaths() to discover submodule git config files using git config --show-origin
  • Added tryGetConfigKeys() to retrieve config keys matching a regex pattern
  • Added tryGetConfigValues() to retrieve all values for a given config key
  • Added tryConfigUnsetValue() to remove specific key-value pairs from git config

__test__/git-auth-helper.test.ts:

  • Added test removeAuth removes v6 style credentials covering main repository cleanup
  • Added test removeAuth removes v6 style credentials from submodules covering submodule cleanup
  • Added test removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set covering the skip flag
  • Updated mock setup to include new git command manager methods

__test__/git-directory-helper.test.ts:

  • Updated mock setup to include new git command manager methods

dist/index.js:

  • Compiled/bundled output reflecting all source changes
sequenceDiagram
    participant Client
    participant GitAuthHelper
    participant GitCommandManager
    participant FileSystem

    Client->>GitAuthHelper: removeAuth()
    GitAuthHelper->>GitAuthHelper: removeGitConfig(tokenConfigKey)
    
    alt SKIP_V6_CLEANUP not set
        GitAuthHelper->>GitAuthHelper: removeIncludeIfCredentials()
        GitAuthHelper->>GitCommandManager: tryGetConfigKeys('^includeIf\\.gitdir:')
        GitCommandManager-->>GitAuthHelper: [keys]
        
        loop for each key
            GitAuthHelper->>GitCommandManager: tryGetConfigValues(key)
            GitCommandManager-->>GitAuthHelper: [paths]
            
            loop for each path matching pattern
                GitAuthHelper->>GitCommandManager: tryConfigUnsetValue(key, path)
                GitCommandManager->>FileSystem: git config --unset
            end
        end
        
        GitAuthHelper->>GitCommandManager: getSubmoduleConfigPaths(true)
        GitCommandManager->>FileSystem: git submodule foreach --recursive
        GitCommandManager-->>GitAuthHelper: [submodule config paths]
        
        loop for each submodule config
            GitAuthHelper->>GitAuthHelper: removeIncludeIfCredentials(configPath)
            Note over GitAuthHelper: Same process as main repo
        end
        
        loop for each credential file
            alt path under RUNNER_TEMP
                GitAuthHelper->>FileSystem: rmRF(credentialsPath)
            end
        end
    else SKIP_V6_CLEANUP set
        GitAuthHelper->>Client: Skip v6 cleanup (debug log)
    end
    
    GitAuthHelper-->>Client: Complete

github-actions[bot] avatar Nov 25 '25 01:11 github-actions[bot]