Compare commits
5 commits
main
...
support-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ef76f1ac | ||
|
|
ca2f66fc96 | ||
|
|
64240115db | ||
|
|
2df30281e1 | ||
|
|
4924300074 |
7 changed files with 939 additions and 967 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -29,11 +29,11 @@ jobs:
|
||||||
- run: npm run format-check
|
- run: npm run format-check
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run test
|
- run: npm run test
|
||||||
- uses: actions/upload-artifact@v7
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist
|
path: dist
|
||||||
- uses: actions/upload-artifact@v7
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: action.yml
|
name: action.yml
|
||||||
path: action.yml
|
path: action.yml
|
||||||
|
|
@ -50,12 +50,12 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist
|
path: dist
|
||||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: action.yml
|
name: action.yml
|
||||||
path: .
|
path: .
|
||||||
|
|
@ -119,7 +119,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/download-artifact@v8
|
- uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist
|
path: dist
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import {GitCommandManager} from '../lib/git-command-manager'
|
import {GitCommandManager} from '../lib/git-command-manager'
|
||||||
import {GitConfigHelper} from '../lib/git-config-helper'
|
import {GitConfigHelper} from '../lib/git-config-helper'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
const REPO_PATH = '/git/local/repos/test-base'
|
const REPO_PATH = '/git/local/repos/test-base'
|
||||||
|
|
||||||
|
|
@ -7,29 +9,104 @@ const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader'
|
||||||
|
|
||||||
describe('git-config-helper integration tests', () => {
|
describe('git-config-helper integration tests', () => {
|
||||||
let git: GitCommandManager
|
let git: GitCommandManager
|
||||||
|
let originalRunnerTemp: string | undefined
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
git = await GitCommandManager.create(REPO_PATH)
|
git = await GitCommandManager.create(REPO_PATH)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Save original RUNNER_TEMP
|
||||||
|
originalRunnerTemp = process.env['RUNNER_TEMP']
|
||||||
|
// Create a temp directory for tests
|
||||||
|
const tempDir = await fs.promises.mkdtemp('/tmp/cpr-test-')
|
||||||
|
process.env['RUNNER_TEMP'] = tempDir
|
||||||
|
process.env['GITHUB_WORKSPACE'] = REPO_PATH
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up RUNNER_TEMP
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP']
|
||||||
|
if (runnerTemp && runnerTemp.startsWith('/tmp/cpr-test-')) {
|
||||||
|
await fs.promises.rm(runnerTemp, {recursive: true, force: true})
|
||||||
|
}
|
||||||
|
// Restore original RUNNER_TEMP
|
||||||
|
if (originalRunnerTemp !== undefined) {
|
||||||
|
process.env['RUNNER_TEMP'] = originalRunnerTemp
|
||||||
|
} else {
|
||||||
|
delete process.env['RUNNER_TEMP']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('tests save and restore with no persisted auth', async () => {
|
it('tests save and restore with no persisted auth', async () => {
|
||||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||||
await gitConfigHelper.close()
|
await gitConfigHelper.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tests configure and removal of auth', async () => {
|
it('tests configure and removal of auth using credentials file', async () => {
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP']!
|
||||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||||
await gitConfigHelper.configureToken('github-token')
|
await gitConfigHelper.configureToken('github-token')
|
||||||
expect(await git.configExists(extraheaderConfigKey)).toBeTruthy()
|
|
||||||
expect(await git.getConfigValue(extraheaderConfigKey)).toEqual(
|
// Verify credentials file was created in RUNNER_TEMP
|
||||||
|
const files = await fs.promises.readdir(runnerTemp)
|
||||||
|
const credentialsFiles = files.filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
|
||||||
|
// Verify credentials file contains the auth token
|
||||||
|
const credentialsPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = await fs.promises.readFile(
|
||||||
|
credentialsPath,
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
expect(credentialsContent).toContain(
|
||||||
'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu'
|
'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Verify includeIf entries were added to local config
|
||||||
|
const includeIfKeys = await git.tryGetConfigKeys('^includeIf\\.gitdir:')
|
||||||
|
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()
|
||||||
expect(await git.configExists(extraheaderConfigKey)).toBeFalsy()
|
|
||||||
|
// Verify credentials file was removed
|
||||||
|
const filesAfter = await fs.promises.readdir(runnerTemp)
|
||||||
|
const credentialsFilesAfter = filesAfter.filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFilesAfter.length).toBe(0)
|
||||||
|
|
||||||
|
// Verify includeIf entries pointing to our specific credentials file were removed
|
||||||
|
const includeIfKeysAfter = await git.tryGetConfigKeys(
|
||||||
|
'^includeIf\\.gitdir:'
|
||||||
|
)
|
||||||
|
let credentialIncludesForThisActionAfter = 0
|
||||||
|
for (const key of includeIfKeysAfter) {
|
||||||
|
const values = await git.tryGetConfigValues(key)
|
||||||
|
for (const value of values) {
|
||||||
|
if (value === credentialsPath) {
|
||||||
|
credentialIncludesForThisActionAfter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(credentialIncludesForThisActionAfter).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tests save and restore of persisted auth', async () => {
|
it('tests save and restore of persisted auth (old-style)', async () => {
|
||||||
const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***'
|
const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***'
|
||||||
await git.config(extraheaderConfigKey, extraheaderConfigValue)
|
await git.config(extraheaderConfigKey, extraheaderConfigValue)
|
||||||
|
|
||||||
|
|
|
||||||
276
dist/index.js
vendored
276
dist/index.js
vendored
|
|
@ -730,9 +730,15 @@ class GitCommandManager {
|
||||||
return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes });
|
return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
config(configKey, configValue, globalConfig, add) {
|
config(configKey, configValue, globalConfig, add, configFile) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const args = ['config', globalConfig ? '--global' : '--local'];
|
const args = ['config'];
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local');
|
||||||
|
}
|
||||||
if (add) {
|
if (add) {
|
||||||
args.push('--add');
|
args.push('--add');
|
||||||
}
|
}
|
||||||
|
|
@ -964,6 +970,60 @@ class GitCommandManager {
|
||||||
return output.exitCode === 0;
|
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('--fixed-value', '--unset', configKey, configValue);
|
||||||
|
const output = yield this.exec(args, { allowAllExitCodes: true });
|
||||||
|
return output.exitCode === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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.exec(args, { allowAllExitCodes: 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.exec(args, { allowAllExitCodes: true });
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(key => key.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
tryGetRemoteUrl() {
|
tryGetRemoteUrl() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const output = yield this.exec(['config', '--local', '--get', 'remote.origin.url'], { allowAllExitCodes: true });
|
const output = yield this.exec(['config', '--local', '--get', 'remote.origin.url'], { allowAllExitCodes: true });
|
||||||
|
|
@ -1100,9 +1160,9 @@ const fs = __importStar(__nccwpck_require__(9896));
|
||||||
const path = __importStar(__nccwpck_require__(6928));
|
const path = __importStar(__nccwpck_require__(6928));
|
||||||
const url_1 = __nccwpck_require__(7016);
|
const url_1 = __nccwpck_require__(7016);
|
||||||
const utils = __importStar(__nccwpck_require__(9277));
|
const utils = __importStar(__nccwpck_require__(9277));
|
||||||
|
const uuid_1 = __nccwpck_require__(2048);
|
||||||
class GitConfigHelper {
|
class GitConfigHelper {
|
||||||
constructor(git) {
|
constructor(git) {
|
||||||
this.gitConfigPath = '';
|
|
||||||
this.safeDirectoryConfigKey = 'safe.directory';
|
this.safeDirectoryConfigKey = 'safe.directory';
|
||||||
this.safeDirectoryAdded = false;
|
this.safeDirectoryAdded = false;
|
||||||
this.remoteUrl = '';
|
this.remoteUrl = '';
|
||||||
|
|
@ -1110,7 +1170,8 @@ class GitConfigHelper {
|
||||||
this.extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***';
|
this.extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***';
|
||||||
this.extraheaderConfigValueRegex = '^AUTHORIZATION:';
|
this.extraheaderConfigValueRegex = '^AUTHORIZATION:';
|
||||||
this.persistedExtraheaderConfigValue = '';
|
this.persistedExtraheaderConfigValue = '';
|
||||||
this.backedUpCredentialFiles = [];
|
// Path to the credentials config file in RUNNER_TEMP (new v6-style auth)
|
||||||
|
this.credentialsConfigPath = '';
|
||||||
this.git = git;
|
this.git = git;
|
||||||
this.workingDirectory = this.git.getWorkingDirectory();
|
this.workingDirectory = this.git.getWorkingDirectory();
|
||||||
}
|
}
|
||||||
|
|
@ -1190,16 +1251,15 @@ class GitConfigHelper {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const serverUrl = new url_1.URL(`https://${this.getGitRemote().hostname}`);
|
const serverUrl = new url_1.URL(`https://${this.getGitRemote().hostname}`);
|
||||||
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`;
|
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`;
|
||||||
// Backup checkout@v6 credential files if they exist
|
// Save and unset persisted extraheader credential in git config if it exists (old-style auth)
|
||||||
yield this.hideCredentialFiles();
|
// Note: checkout@v6 uses credentials files with includeIf, so we don't need to
|
||||||
// Save and unset persisted extraheader credential in git config if it exists
|
// manipulate those - they work independently via git's include mechanism
|
||||||
this.persistedExtraheaderConfigValue = yield this.getAndUnset();
|
this.persistedExtraheaderConfigValue = yield this.getAndUnset();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
restorePersistedAuth() {
|
restorePersistedAuth() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Restore checkout@v6 credential files if they were backed up
|
// Restore old-style extraheader config if it was persisted
|
||||||
yield this.unhideCredentialFiles();
|
|
||||||
if (this.persistedExtraheaderConfigValue) {
|
if (this.persistedExtraheaderConfigValue) {
|
||||||
try {
|
try {
|
||||||
yield this.setExtraheaderConfig(this.persistedExtraheaderConfigValue);
|
yield this.setExtraheaderConfig(this.persistedExtraheaderConfigValue);
|
||||||
|
|
@ -1213,69 +1273,160 @@ class GitConfigHelper {
|
||||||
}
|
}
|
||||||
configureToken(token) {
|
configureToken(token) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Encode and configure the basic credential for HTTPS access
|
// Encode the basic credential for HTTPS access
|
||||||
const basicCredential = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64');
|
const basicCredential = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64');
|
||||||
core.setSecret(basicCredential);
|
core.setSecret(basicCredential);
|
||||||
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`;
|
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`;
|
||||||
yield this.setExtraheaderConfig(extraheaderConfigValue);
|
// Get or create the credentials config file path
|
||||||
|
const credentialsConfigPath = this.getCredentialsConfigPath();
|
||||||
|
// Write placeholder to the separate credentials config file using git config.
|
||||||
|
// This approach avoids the credential being captured by process creation audit events,
|
||||||
|
// which are commonly logged. For more information, refer to
|
||||||
|
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
|
yield this.git.config(this.extraheaderConfigKey, this.extraheaderConfigPlaceholderValue, false, // globalConfig
|
||||||
|
false, // add
|
||||||
|
credentialsConfigPath);
|
||||||
|
// Replace the placeholder in the credentials config file
|
||||||
|
let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
|
||||||
|
const placeholderIndex = content.indexOf(this.extraheaderConfigPlaceholderValue);
|
||||||
|
if (placeholderIndex < 0 ||
|
||||||
|
placeholderIndex !=
|
||||||
|
content.lastIndexOf(this.extraheaderConfigPlaceholderValue)) {
|
||||||
|
throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
|
||||||
|
}
|
||||||
|
content = content.replace(this.extraheaderConfigPlaceholderValue, extraheaderConfigValue);
|
||||||
|
yield fs.promises.writeFile(credentialsConfigPath, content);
|
||||||
|
// Configure includeIf entries to reference the credentials config file
|
||||||
|
yield this.configureIncludeIf(credentialsConfigPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
removeAuth() {
|
removeAuth() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Remove old-style extraheader config if it exists
|
||||||
yield this.getAndUnset();
|
yield this.getAndUnset();
|
||||||
|
// Remove includeIf entries that point to git-credentials-*.config files
|
||||||
|
// and clean up the credentials config files
|
||||||
|
yield this.removeIncludeIfCredentials();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||||
|
* @returns The absolute path to the credentials config file
|
||||||
|
*/
|
||||||
|
getCredentialsConfigPath() {
|
||||||
|
if (this.credentialsConfigPath) {
|
||||||
|
return this.credentialsConfigPath;
|
||||||
|
}
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || '';
|
||||||
|
if (!runnerTemp) {
|
||||||
|
throw new Error('RUNNER_TEMP is not defined');
|
||||||
|
}
|
||||||
|
// Create a unique filename for this action instance
|
||||||
|
const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
|
||||||
|
this.credentialsConfigPath = path.join(runnerTemp, configFileName);
|
||||||
|
core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
|
||||||
|
return this.credentialsConfigPath;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Configures includeIf entries in the local git config to reference the credentials file.
|
||||||
|
* Sets up entries for both host and container paths to support Docker container actions.
|
||||||
|
*/
|
||||||
|
configureIncludeIf(credentialsConfigPath) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Host git directory
|
||||||
|
const gitDir = yield this.git.getGitDirectory();
|
||||||
|
let hostGitDir = path.join(this.workingDirectory, gitDir);
|
||||||
|
hostGitDir = hostGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
// Configure host includeIf
|
||||||
|
const hostIncludeKey = `includeIf.gitdir:${hostGitDir}.path`;
|
||||||
|
yield this.git.config(hostIncludeKey, credentialsConfigPath);
|
||||||
|
// Configure host includeIf for worktrees
|
||||||
|
const hostWorktreeIncludeKey = `includeIf.gitdir:${hostGitDir}/worktrees/*.path`;
|
||||||
|
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
|
||||||
|
// Container paths for Docker container actions
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
|
||||||
|
if (githubWorkspace) {
|
||||||
|
let relativePath = path.relative(githubWorkspace, this.workingDirectory);
|
||||||
|
relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
|
||||||
|
// Container credentials config path
|
||||||
|
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
|
||||||
|
// Configure container includeIf
|
||||||
|
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
|
||||||
|
yield this.git.config(containerIncludeKey, containerCredentialsPath);
|
||||||
|
// Configure container includeIf for worktrees
|
||||||
|
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
|
||||||
|
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes the includeIf entry and credentials config file created by this action instance.
|
||||||
|
* Only cleans up the specific credentials file tracked in this.credentialsConfigPath,
|
||||||
|
* leaving credentials created by other actions (e.g., actions/checkout) intact.
|
||||||
|
*/
|
||||||
|
removeIncludeIfCredentials() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Only clean up if this action instance created a credentials config file
|
||||||
|
if (!this.credentialsConfigPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Get all includeIf.gitdir keys from local config
|
||||||
|
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:');
|
||||||
|
for (const key of keys) {
|
||||||
|
// Get all values for this key
|
||||||
|
const values = yield this.git.tryGetConfigValues(key);
|
||||||
|
for (const value of values) {
|
||||||
|
// Only remove entries pointing to our specific credentials file
|
||||||
|
if (value === this.credentialsConfigPath) {
|
||||||
|
yield this.git.tryConfigUnsetValue(key, value);
|
||||||
|
core.debug(`Removed includeIf entry: ${key} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`);
|
||||||
|
}
|
||||||
|
// Delete only our credentials config file
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'];
|
||||||
|
const resolvedCredentialsPath = path.resolve(this.credentialsConfigPath);
|
||||||
|
const resolvedRunnerTemp = runnerTemp ? path.resolve(runnerTemp) : '';
|
||||||
|
if (resolvedRunnerTemp &&
|
||||||
|
resolvedCredentialsPath.startsWith(resolvedRunnerTemp + path.sep)) {
|
||||||
|
try {
|
||||||
|
yield fs.promises.unlink(this.credentialsConfigPath);
|
||||||
|
core.info(`Removed credentials config file: ${this.credentialsConfigPath}`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
core.debug(`Could not remove credentials file ${this.credentialsConfigPath}: ${utils.getErrorMessage(e)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets extraheader config directly in .git/config (old-style auth).
|
||||||
|
* Used only for restoring persisted credentials from checkout@v4/v5.
|
||||||
|
*/
|
||||||
setExtraheaderConfig(extraheaderConfigValue) {
|
setExtraheaderConfig(extraheaderConfigValue) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
// Configure a placeholder value. This approach avoids the credential being captured
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
// by process creation audit events, which are commonly logged. For more information,
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
// See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274
|
|
||||||
yield this.git.config(this.extraheaderConfigKey, this.extraheaderConfigPlaceholderValue);
|
yield this.git.config(this.extraheaderConfigKey, this.extraheaderConfigPlaceholderValue);
|
||||||
// Replace the placeholder
|
// Replace the placeholder in the local git config
|
||||||
yield this.gitConfigStringReplace(this.extraheaderConfigPlaceholderValue, extraheaderConfigValue);
|
const gitDir = yield this.git.getGitDirectory();
|
||||||
});
|
const gitConfigPath = path.join(this.workingDirectory, gitDir, 'config');
|
||||||
}
|
let content = (yield fs.promises.readFile(gitConfigPath)).toString();
|
||||||
hideCredentialFiles() {
|
const index = content.indexOf(this.extraheaderConfigPlaceholderValue);
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
if (index < 0 ||
|
||||||
// Temporarily hide checkout@v6 credential files to avoid duplicate auth headers
|
index != content.lastIndexOf(this.extraheaderConfigPlaceholderValue)) {
|
||||||
const runnerTemp = process.env['RUNNER_TEMP'];
|
throw new Error(`Unable to replace '${this.extraheaderConfigPlaceholderValue}' in ${gitConfigPath}`);
|
||||||
if (!runnerTemp) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
content = content.replace(this.extraheaderConfigPlaceholderValue, extraheaderConfigValue);
|
||||||
const files = yield fs.promises.readdir(runnerTemp);
|
yield fs.promises.writeFile(gitConfigPath, content);
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('git-credentials-') && file.endsWith('.config')) {
|
|
||||||
const sourcePath = path.join(runnerTemp, file);
|
|
||||||
const backupPath = `${sourcePath}.bak`;
|
|
||||||
yield fs.promises.rename(sourcePath, backupPath);
|
|
||||||
this.backedUpCredentialFiles.push(backupPath);
|
|
||||||
core.info(`Temporarily hiding checkout credential file: ${file} (will be restored after)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// If directory doesn't exist or we can't read it, just continue
|
|
||||||
core.debug(`Could not backup credential files: ${utils.getErrorMessage(e)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
unhideCredentialFiles() {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
// Restore checkout@v6 credential files that were backed up
|
|
||||||
for (const backupPath of this.backedUpCredentialFiles) {
|
|
||||||
try {
|
|
||||||
const originalPath = backupPath.replace(/\.bak$/, '');
|
|
||||||
yield fs.promises.rename(backupPath, originalPath);
|
|
||||||
const fileName = path.basename(originalPath);
|
|
||||||
core.info(`Restored checkout credential file: ${fileName}`);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
core.warning(`Failed to restore credential file ${backupPath}: ${utils.getErrorMessage(e)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.backedUpCredentialFiles = [];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getAndUnset() {
|
getAndUnset() {
|
||||||
|
|
@ -1294,21 +1445,6 @@ class GitConfigHelper {
|
||||||
return configValue;
|
return configValue;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
gitConfigStringReplace(find, replace) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
if (this.gitConfigPath.length === 0) {
|
|
||||||
const gitDir = yield this.git.getGitDirectory();
|
|
||||||
this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config');
|
|
||||||
}
|
|
||||||
let content = (yield fs.promises.readFile(this.gitConfigPath)).toString();
|
|
||||||
const index = content.indexOf(find);
|
|
||||||
if (index < 0 || index != content.lastIndexOf(find)) {
|
|
||||||
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`);
|
|
||||||
}
|
|
||||||
content = content.replace(find, replace);
|
|
||||||
yield fs.promises.writeFile(this.gitConfigPath, content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
exports.GitConfigHelper = GitConfigHelper;
|
exports.GitConfigHelper = GitConfigHelper;
|
||||||
|
|
||||||
|
|
|
||||||
1188
package-lock.json
generated
1188
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -54,12 +54,12 @@
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jest": "^27.9.0",
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-circus": "^29.7.0",
|
"jest-circus": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.7.4",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"undici": "^6.23.0"
|
"undici": "^6.23.0"
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,15 @@ export class GitCommandManager {
|
||||||
configKey: string,
|
configKey: string,
|
||||||
configValue: string,
|
configValue: string,
|
||||||
globalConfig?: boolean,
|
globalConfig?: boolean,
|
||||||
add?: boolean
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
const args: string[] = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
if (add) {
|
if (add) {
|
||||||
args.push('--add')
|
args.push('--add')
|
||||||
}
|
}
|
||||||
|
|
@ -350,6 +356,67 @@ export class GitCommandManager {
|
||||||
return output.exitCode === 0
|
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('--fixed-value', '--unset', configKey, configValue)
|
||||||
|
const output = await this.exec(args, {allowAllExitCodes: true})
|
||||||
|
return output.exitCode === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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.exec(args, {allowAllExitCodes: 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.exec(args, {allowAllExitCodes: true})
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(key => key.trim())
|
||||||
|
}
|
||||||
|
|
||||||
async tryGetRemoteUrl(): Promise<string> {
|
async tryGetRemoteUrl(): Promise<string> {
|
||||||
const output = await this.exec(
|
const output = await this.exec(
|
||||||
['config', '--local', '--get', 'remote.origin.url'],
|
['config', '--local', '--get', 'remote.origin.url'],
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {GitCommandManager} from './git-command-manager'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {URL} from 'url'
|
import {URL} from 'url'
|
||||||
import * as utils from './utils'
|
import * as utils from './utils'
|
||||||
|
import {v4 as uuid} from 'uuid'
|
||||||
|
|
||||||
interface GitRemote {
|
interface GitRemote {
|
||||||
hostname: string
|
hostname: string
|
||||||
|
|
@ -13,7 +14,6 @@ interface GitRemote {
|
||||||
|
|
||||||
export class GitConfigHelper {
|
export class GitConfigHelper {
|
||||||
private git: GitCommandManager
|
private git: GitCommandManager
|
||||||
private gitConfigPath = ''
|
|
||||||
private workingDirectory: string
|
private workingDirectory: string
|
||||||
private safeDirectoryConfigKey = 'safe.directory'
|
private safeDirectoryConfigKey = 'safe.directory'
|
||||||
private safeDirectoryAdded = false
|
private safeDirectoryAdded = false
|
||||||
|
|
@ -22,7 +22,8 @@ export class GitConfigHelper {
|
||||||
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
||||||
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
||||||
private persistedExtraheaderConfigValue = ''
|
private persistedExtraheaderConfigValue = ''
|
||||||
private backedUpCredentialFiles: string[] = []
|
// Path to the credentials config file in RUNNER_TEMP (new v6-style auth)
|
||||||
|
private credentialsConfigPath = ''
|
||||||
|
|
||||||
private constructor(git: GitCommandManager) {
|
private constructor(git: GitCommandManager) {
|
||||||
this.git = git
|
this.git = git
|
||||||
|
|
@ -122,15 +123,14 @@ export class GitConfigHelper {
|
||||||
async savePersistedAuth(): Promise<void> {
|
async savePersistedAuth(): Promise<void> {
|
||||||
const serverUrl = new URL(`https://${this.getGitRemote().hostname}`)
|
const serverUrl = new URL(`https://${this.getGitRemote().hostname}`)
|
||||||
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
||||||
// Backup checkout@v6 credential files if they exist
|
// Save and unset persisted extraheader credential in git config if it exists (old-style auth)
|
||||||
await this.hideCredentialFiles()
|
// Note: checkout@v6 uses credentials files with includeIf, so we don't need to
|
||||||
// Save and unset persisted extraheader credential in git config if it exists
|
// manipulate those - they work independently via git's include mechanism
|
||||||
this.persistedExtraheaderConfigValue = await this.getAndUnset()
|
this.persistedExtraheaderConfigValue = await this.getAndUnset()
|
||||||
}
|
}
|
||||||
|
|
||||||
async restorePersistedAuth(): Promise<void> {
|
async restorePersistedAuth(): Promise<void> {
|
||||||
// Restore checkout@v6 credential files if they were backed up
|
// Restore old-style extraheader config if it was persisted
|
||||||
await this.unhideCredentialFiles()
|
|
||||||
if (this.persistedExtraheaderConfigValue) {
|
if (this.persistedExtraheaderConfigValue) {
|
||||||
try {
|
try {
|
||||||
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
|
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
|
||||||
|
|
@ -142,81 +142,218 @@ export class GitConfigHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
async configureToken(token: string): Promise<void> {
|
async configureToken(token: string): Promise<void> {
|
||||||
// Encode and configure the basic credential for HTTPS access
|
// Encode the basic credential for HTTPS access
|
||||||
const basicCredential = Buffer.from(
|
const basicCredential = Buffer.from(
|
||||||
`x-access-token:${token}`,
|
`x-access-token:${token}`,
|
||||||
'utf8'
|
'utf8'
|
||||||
).toString('base64')
|
).toString('base64')
|
||||||
core.setSecret(basicCredential)
|
core.setSecret(basicCredential)
|
||||||
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`
|
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`
|
||||||
await this.setExtraheaderConfig(extraheaderConfigValue)
|
|
||||||
|
// Get or create the credentials config file path
|
||||||
|
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||||
|
|
||||||
|
// Write placeholder to the separate credentials config file using git config.
|
||||||
|
// This approach avoids the credential being captured by process creation audit events,
|
||||||
|
// which are commonly logged. For more information, refer to
|
||||||
|
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
|
await this.git.config(
|
||||||
|
this.extraheaderConfigKey,
|
||||||
|
this.extraheaderConfigPlaceholderValue,
|
||||||
|
false, // globalConfig
|
||||||
|
false, // add
|
||||||
|
credentialsConfigPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replace the placeholder in the credentials config file
|
||||||
|
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
|
||||||
|
const placeholderIndex = content.indexOf(
|
||||||
|
this.extraheaderConfigPlaceholderValue
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
placeholderIndex < 0 ||
|
||||||
|
placeholderIndex !=
|
||||||
|
content.lastIndexOf(this.extraheaderConfigPlaceholderValue)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content = content.replace(
|
||||||
|
this.extraheaderConfigPlaceholderValue,
|
||||||
|
extraheaderConfigValue
|
||||||
|
)
|
||||||
|
await fs.promises.writeFile(credentialsConfigPath, content)
|
||||||
|
|
||||||
|
// Configure includeIf entries to reference the credentials config file
|
||||||
|
await this.configureIncludeIf(credentialsConfigPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAuth(): Promise<void> {
|
async removeAuth(): Promise<void> {
|
||||||
|
// Remove old-style extraheader config if it exists
|
||||||
await this.getAndUnset()
|
await this.getAndUnset()
|
||||||
|
|
||||||
|
// Remove includeIf entries that point to git-credentials-*.config files
|
||||||
|
// and clean up the credentials config files
|
||||||
|
await this.removeIncludeIfCredentials()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||||
|
* @returns The absolute path to the credentials config file
|
||||||
|
*/
|
||||||
|
private getCredentialsConfigPath(): string {
|
||||||
|
if (this.credentialsConfigPath) {
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
|
if (!runnerTemp) {
|
||||||
|
throw new Error('RUNNER_TEMP is not defined')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique filename for this action instance
|
||||||
|
const configFileName = `git-credentials-${uuid()}.config`
|
||||||
|
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
|
||||||
|
|
||||||
|
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures includeIf entries in the local git config to reference the credentials file.
|
||||||
|
* Sets up entries for both host and container paths to support Docker container actions.
|
||||||
|
*/
|
||||||
|
private async configureIncludeIf(
|
||||||
|
credentialsConfigPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Host git directory
|
||||||
|
const gitDir = await this.git.getGitDirectory()
|
||||||
|
let hostGitDir = path.join(this.workingDirectory, gitDir)
|
||||||
|
hostGitDir = hostGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
|
// Configure host includeIf
|
||||||
|
const hostIncludeKey = `includeIf.gitdir:${hostGitDir}.path`
|
||||||
|
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
|
// Configure host includeIf for worktrees
|
||||||
|
const hostWorktreeIncludeKey = `includeIf.gitdir:${hostGitDir}/worktrees/*.path`
|
||||||
|
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
|
// Container paths for Docker container actions
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
if (githubWorkspace) {
|
||||||
|
let relativePath = path.relative(githubWorkspace, this.workingDirectory)
|
||||||
|
relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
const containerGitDir = path.posix.join(
|
||||||
|
'/github/workspace',
|
||||||
|
relativePath,
|
||||||
|
'.git'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container credentials config path
|
||||||
|
const containerCredentialsPath = path.posix.join(
|
||||||
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsConfigPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure container includeIf
|
||||||
|
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
|
||||||
|
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
||||||
|
|
||||||
|
// Configure container includeIf for worktrees
|
||||||
|
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
||||||
|
await this.git.config(
|
||||||
|
containerWorktreeIncludeKey,
|
||||||
|
containerCredentialsPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the includeIf entry and credentials config file created by this action instance.
|
||||||
|
* 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> {
|
||||||
|
// Only clean up if this action instance created a credentials config file
|
||||||
|
if (!this.credentialsConfigPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all includeIf.gitdir keys from local config
|
||||||
|
const keys = await this.git.tryGetConfigKeys('^includeIf\\.gitdir:')
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
// Get all values for this key
|
||||||
|
const values = await this.git.tryGetConfigValues(key)
|
||||||
|
for (const value of values) {
|
||||||
|
// Only remove entries pointing to our specific credentials file
|
||||||
|
if (value === this.credentialsConfigPath) {
|
||||||
|
await this.git.tryConfigUnsetValue(key, value)
|
||||||
|
core.debug(`Removed includeIf entry: ${key} = ${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
core.debug(`Error during includeIf cleanup: ${utils.getErrorMessage(e)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete only our credentials config file
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP']
|
||||||
|
const resolvedCredentialsPath = path.resolve(this.credentialsConfigPath)
|
||||||
|
const resolvedRunnerTemp = runnerTemp ? path.resolve(runnerTemp) : ''
|
||||||
|
if (
|
||||||
|
resolvedRunnerTemp &&
|
||||||
|
resolvedCredentialsPath.startsWith(resolvedRunnerTemp + path.sep)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(this.credentialsConfigPath)
|
||||||
|
core.info(
|
||||||
|
`Removed credentials config file: ${this.credentialsConfigPath}`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
core.debug(
|
||||||
|
`Could not remove credentials file ${this.credentialsConfigPath}: ${utils.getErrorMessage(e)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets extraheader config directly in .git/config (old-style auth).
|
||||||
|
* Used only for restoring persisted credentials from checkout@v4/v5.
|
||||||
|
*/
|
||||||
private async setExtraheaderConfig(
|
private async setExtraheaderConfig(
|
||||||
extraheaderConfigValue: string
|
extraheaderConfigValue: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
// Configure a placeholder value. This approach avoids the credential being captured
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
// by process creation audit events, which are commonly logged. For more information,
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
// See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274
|
|
||||||
await this.git.config(
|
await this.git.config(
|
||||||
this.extraheaderConfigKey,
|
this.extraheaderConfigKey,
|
||||||
this.extraheaderConfigPlaceholderValue
|
this.extraheaderConfigPlaceholderValue
|
||||||
)
|
)
|
||||||
// Replace the placeholder
|
// Replace the placeholder in the local git config
|
||||||
await this.gitConfigStringReplace(
|
const gitDir = await this.git.getGitDirectory()
|
||||||
|
const gitConfigPath = path.join(this.workingDirectory, gitDir, 'config')
|
||||||
|
let content = (await fs.promises.readFile(gitConfigPath)).toString()
|
||||||
|
const index = content.indexOf(this.extraheaderConfigPlaceholderValue)
|
||||||
|
if (
|
||||||
|
index < 0 ||
|
||||||
|
index != content.lastIndexOf(this.extraheaderConfigPlaceholderValue)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to replace '${this.extraheaderConfigPlaceholderValue}' in ${gitConfigPath}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content = content.replace(
|
||||||
this.extraheaderConfigPlaceholderValue,
|
this.extraheaderConfigPlaceholderValue,
|
||||||
extraheaderConfigValue
|
extraheaderConfigValue
|
||||||
)
|
)
|
||||||
}
|
await fs.promises.writeFile(gitConfigPath, content)
|
||||||
|
|
||||||
private async hideCredentialFiles(): Promise<void> {
|
|
||||||
// Temporarily hide checkout@v6 credential files to avoid duplicate auth headers
|
|
||||||
const runnerTemp = process.env['RUNNER_TEMP']
|
|
||||||
if (!runnerTemp) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.promises.readdir(runnerTemp)
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('git-credentials-') && file.endsWith('.config')) {
|
|
||||||
const sourcePath = path.join(runnerTemp, file)
|
|
||||||
const backupPath = `${sourcePath}.bak`
|
|
||||||
await fs.promises.rename(sourcePath, backupPath)
|
|
||||||
this.backedUpCredentialFiles.push(backupPath)
|
|
||||||
core.info(
|
|
||||||
`Temporarily hiding checkout credential file: ${file} (will be restored after)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// If directory doesn't exist or we can't read it, just continue
|
|
||||||
core.debug(
|
|
||||||
`Could not backup credential files: ${utils.getErrorMessage(e)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async unhideCredentialFiles(): Promise<void> {
|
|
||||||
// Restore checkout@v6 credential files that were backed up
|
|
||||||
for (const backupPath of this.backedUpCredentialFiles) {
|
|
||||||
try {
|
|
||||||
const originalPath = backupPath.replace(/\.bak$/, '')
|
|
||||||
await fs.promises.rename(backupPath, originalPath)
|
|
||||||
const fileName = path.basename(originalPath)
|
|
||||||
core.info(`Restored checkout credential file: ${fileName}`)
|
|
||||||
} catch (e) {
|
|
||||||
core.warning(
|
|
||||||
`Failed to restore credential file ${backupPath}: ${utils.getErrorMessage(e)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.backedUpCredentialFiles = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAndUnset(): Promise<string> {
|
private async getAndUnset(): Promise<string> {
|
||||||
|
|
@ -247,21 +384,4 @@ export class GitConfigHelper {
|
||||||
}
|
}
|
||||||
return configValue
|
return configValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private async gitConfigStringReplace(
|
|
||||||
find: string,
|
|
||||||
replace: string
|
|
||||||
): Promise<void> {
|
|
||||||
if (this.gitConfigPath.length === 0) {
|
|
||||||
const gitDir = await this.git.getGitDirectory()
|
|
||||||
this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config')
|
|
||||||
}
|
|
||||||
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
|
|
||||||
const index = content.indexOf(find)
|
|
||||||
if (index < 0 || index != content.lastIndexOf(find)) {
|
|
||||||
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`)
|
|
||||||
}
|
|
||||||
content = content.replace(find, replace)
|
|
||||||
await fs.promises.writeFile(this.gitConfigPath, content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue