fix: only remove credentials created by this action instance

Previously, removeIncludeIfCredentials() deleted all includeIf.gitdir
entries matching git-credentials-*.config, regardless of which action
created them. This broke subsequent workflow steps that relied on
credentials persisted by other actions (e.g., actions/checkout@v6).

Now the cleanup only removes the specific credentials file and config
entries created by this action instance, leaving other actions'
credentials intact.
This commit is contained in:
Peter Evans 2026-01-23 09:43:33 +00:00
parent 64240115db
commit ca2f66fc96
3 changed files with 54 additions and 59 deletions

View file

@ -69,6 +69,18 @@ describe('git-config-helper integration tests', () => {
const includeIfKeys = await git.tryGetConfigKeys('^includeIf\\.gitdir:') const includeIfKeys = await git.tryGetConfigKeys('^includeIf\\.gitdir:')
expect(includeIfKeys.length).toBeGreaterThan(0) expect(includeIfKeys.length).toBeGreaterThan(0)
// Count credential includes pointing to this action's credentials file
let credentialIncludesForThisAction = 0
for (const key of includeIfKeys) {
const values = await git.tryGetConfigValues(key)
for (const value of values) {
if (value === credentialsPath) {
credentialIncludesForThisAction++
}
}
}
expect(credentialIncludesForThisAction).toBeGreaterThan(0)
await gitConfigHelper.close() await gitConfigHelper.close()
// Verify credentials file was removed // Verify credentials file was removed
@ -78,20 +90,20 @@ describe('git-config-helper integration tests', () => {
) )
expect(credentialsFilesAfter.length).toBe(0) expect(credentialsFilesAfter.length).toBe(0)
// Verify includeIf entries were removed // Verify includeIf entries pointing to our specific credentials file were removed
const includeIfKeysAfter = await git.tryGetConfigKeys( const includeIfKeysAfter = await git.tryGetConfigKeys(
'^includeIf\\.gitdir:' '^includeIf\\.gitdir:'
) )
const credentialIncludes: string[] = [] let credentialIncludesForThisActionAfter = 0
for (const key of includeIfKeysAfter) { for (const key of includeIfKeysAfter) {
const values = await git.tryGetConfigValues(key) const values = await git.tryGetConfigValues(key)
for (const value of values) { for (const value of values) {
if (/git-credentials-[0-9a-f-]+\.config$/i.test(value)) { if (value === credentialsPath) {
credentialIncludes.push(value) credentialIncludesForThisActionAfter++
} }
} }
} }
expect(credentialIncludes.length).toBe(0) expect(credentialIncludesForThisActionAfter).toBe(0)
}) })
it('tests save and restore of persisted auth (old-style)', async () => { it('tests save and restore of persisted auth (old-style)', async () => {

44
dist/index.js vendored
View file

@ -979,8 +979,6 @@ class GitCommandManager {
else { else {
args.push(globalConfig ? '--global' : '--local'); args.push(globalConfig ? '--global' : '--local');
} }
// Use --fixed-value to treat configValue as a literal string, not a regex pattern.
// This is important for file paths which contain regex special characters like '.'
args.push('--fixed-value', '--unset', configKey, configValue); args.push('--fixed-value', '--unset', configKey, configValue);
const output = yield this.exec(args, { allowAllExitCodes: true }); const output = yield this.exec(args, { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
@ -1363,12 +1361,16 @@ class GitConfigHelper {
}); });
} }
/** /**
* Removes includeIf entries that point to git-credentials-*.config files * Removes the includeIf entry and credentials config file created by this action instance.
* and deletes the credentials config files. * Only cleans up the specific credentials file tracked in this.credentialsConfigPath,
* leaving credentials created by other actions (e.g., actions/checkout) intact.
*/ */
removeIncludeIfCredentials() { removeIncludeIfCredentials() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const credentialsPaths = new Set(); // Only clean up if this action instance created a credentials config file
if (!this.credentialsConfigPath) {
return;
}
try { try {
// Get all includeIf.gitdir keys from local config // Get all includeIf.gitdir keys from local config
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:'); const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:');
@ -1376,9 +1378,8 @@ class GitConfigHelper {
// Get all values for this key // Get all values for this key
const values = yield this.git.tryGetConfigValues(key); const values = yield this.git.tryGetConfigValues(key);
for (const value of values) { for (const value of values) {
// Check if value matches git-credentials-<uuid>.config pattern // Only remove entries pointing to our specific credentials file
if (this.isCredentialsConfigPath(value)) { if (value === this.credentialsConfigPath) {
credentialsPaths.add(value);
yield this.git.tryConfigUnsetValue(key, value); yield this.git.tryConfigUnsetValue(key, value);
core.debug(`Removed includeIf entry: ${key} = ${value}`); core.debug(`Removed includeIf entry: ${key} = ${value}`);
} }
@ -1389,30 +1390,19 @@ class GitConfigHelper {
// Ignore errors during cleanup // Ignore errors during cleanup
core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`); core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`);
} }
// Delete credentials config files that are under RUNNER_TEMP // Delete only our credentials config file
const runnerTemp = process.env['RUNNER_TEMP']; const runnerTemp = process.env['RUNNER_TEMP'];
if (runnerTemp) { if (runnerTemp && this.credentialsConfigPath.startsWith(runnerTemp)) {
for (const credentialsPath of credentialsPaths) { try {
// Only remove files under RUNNER_TEMP for safety yield fs.promises.unlink(this.credentialsConfigPath);
if (credentialsPath.startsWith(runnerTemp)) { core.info(`Removed credentials config file: ${this.credentialsConfigPath}`);
try { }
yield fs.promises.unlink(credentialsPath); catch (e) {
core.info(`Removed credentials config file: ${credentialsPath}`); core.debug(`Could not remove credentials file ${this.credentialsConfigPath}: ${utils.getErrorMessage(e)}`);
}
catch (e) {
core.debug(`Could not remove credentials file ${credentialsPath}: ${utils.getErrorMessage(e)}`);
}
}
} }
} }
}); });
} }
/**
* Tests if a path matches the git-credentials-*.config pattern.
*/
isCredentialsConfigPath(filePath) {
return /git-credentials-[0-9a-f-]+\.config$/i.test(filePath);
}
/** /**
* Sets extraheader config directly in .git/config (old-style auth). * Sets extraheader config directly in .git/config (old-style auth).
* Used only for restoring persisted credentials from checkout@v4/v5. * Used only for restoring persisted credentials from checkout@v4/v5.

View file

@ -271,11 +271,15 @@ export class GitConfigHelper {
} }
/** /**
* Removes includeIf entries that point to git-credentials-*.config files * Removes the includeIf entry and credentials config file created by this action instance.
* and deletes the credentials config files. * Only cleans up the specific credentials file tracked in this.credentialsConfigPath,
* leaving credentials created by other actions (e.g., actions/checkout) intact.
*/ */
private async removeIncludeIfCredentials(): Promise<void> { private async removeIncludeIfCredentials(): Promise<void> {
const credentialsPaths = new Set<string>() // Only clean up if this action instance created a credentials config file
if (!this.credentialsConfigPath) {
return
}
try { try {
// Get all includeIf.gitdir keys from local config // Get all includeIf.gitdir keys from local config
@ -285,9 +289,8 @@ export class GitConfigHelper {
// Get all values for this key // Get all values for this key
const values = await this.git.tryGetConfigValues(key) const values = await this.git.tryGetConfigValues(key)
for (const value of values) { for (const value of values) {
// Check if value matches git-credentials-<uuid>.config pattern // Only remove entries pointing to our specific credentials file
if (this.isCredentialsConfigPath(value)) { if (value === this.credentialsConfigPath) {
credentialsPaths.add(value)
await this.git.tryConfigUnsetValue(key, value) await this.git.tryConfigUnsetValue(key, value)
core.debug(`Removed includeIf entry: ${key} = ${value}`) core.debug(`Removed includeIf entry: ${key} = ${value}`)
} }
@ -298,32 +301,22 @@ export class GitConfigHelper {
core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`) core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`)
} }
// Delete credentials config files that are under RUNNER_TEMP // Delete only our credentials config file
const runnerTemp = process.env['RUNNER_TEMP'] const runnerTemp = process.env['RUNNER_TEMP']
if (runnerTemp) { if (runnerTemp && this.credentialsConfigPath.startsWith(runnerTemp)) {
for (const credentialsPath of credentialsPaths) { try {
// Only remove files under RUNNER_TEMP for safety await fs.promises.unlink(this.credentialsConfigPath)
if (credentialsPath.startsWith(runnerTemp)) { core.info(
try { `Removed credentials config file: ${this.credentialsConfigPath}`
await fs.promises.unlink(credentialsPath) )
core.info(`Removed credentials config file: ${credentialsPath}`) } catch (e) {
} catch (e) { core.debug(
core.debug( `Could not remove credentials file ${this.credentialsConfigPath}: ${utils.getErrorMessage(e)}`
`Could not remove credentials file ${credentialsPath}: ${utils.getErrorMessage(e)}` )
)
}
}
} }
} }
} }
/**
* Tests if a path matches the git-credentials-*.config pattern.
*/
private isCredentialsConfigPath(filePath: string): boolean {
return /git-credentials-[0-9a-f-]+\.config$/i.test(filePath)
}
/** /**
* Sets extraheader config directly in .git/config (old-style auth). * Sets extraheader config directly in .git/config (old-style auth).
* Used only for restoring persisted credentials from checkout@v4/v5. * Used only for restoring persisted credentials from checkout@v4/v5.