From f25a3a9f25bd5f4c5d77189cab02ff357b5aedeb Mon Sep 17 00:00:00 2001 From: Thomas Boop <52323235+thboop@users.noreply.github.com> Date: Thu, 14 Apr 2022 12:12:00 -0400 Subject: [PATCH] Safe Directory v2 update (#764) * set safe directory when running checkout --- CHANGELOG.md | 3 + __test__/git-auth-helper.test.ts | 9 +- dist/index.js | 169 +++++++++++++++++------------ src/git-auth-helper.ts | 52 ++++++--- src/git-source-provider.ts | 179 ++++++++++++++++--------------- 5 files changed, 243 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f40def..5302870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v2.4.1 +- [Set the safe directory option on git to prevent git commands failing when running in containers](https://github.com/actions/checkout/pull/762) + ## v2.3.1 - [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index e14e948..80ccbcb 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -643,10 +643,11 @@ describe('git-auth-helper tests', () => { expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) }) - const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override' - it(removeGlobalAuth_removesOverride, async () => { + const removeGlobalConfig_removesOverride = + 'removeGlobalConfig removes override' + it(removeGlobalConfig_removesOverride, async () => { // Arrange - await setup(removeGlobalAuth_removesOverride) + await setup(removeGlobalConfig_removesOverride) const authHelper = gitAuthHelper.createAuthHelper(git, settings) await authHelper.configureAuth() await authHelper.configureGlobalAuth() @@ -655,7 +656,7 @@ describe('git-auth-helper tests', () => { await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig')) // Act - await authHelper.removeGlobalAuth() + await authHelper.removeGlobalConfig() // Assert expect(git.env['HOME']).toBeUndefined() diff --git a/dist/index.js b/dist/index.js index 8542f7d..7bb2e23 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6572,9 +6572,13 @@ class GitAuthHelper { yield this.configureToken(); }); } - configureGlobalAuth() { - var _a; + configureTempGlobalConfig(repositoryPath) { + var _a, _b; return __awaiter(this, void 0, void 0, function* () { + // Already setup global config + if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) { + return path.join(this.temporaryHomePath, '.gitconfig'); + } // Create a temp home directory const runnerTemp = process.env['RUNNER_TEMP'] || ''; assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); @@ -6590,7 +6594,7 @@ class GitAuthHelper { configExists = true; } catch (err) { - if (((_a = err) === null || _a === void 0 ? void 0 : _a.code) !== 'ENOENT') { + if (((_b = err) === null || _b === void 0 ? void 0 : _b.code) !== 'ENOENT') { throw err; } } @@ -6601,10 +6605,25 @@ class GitAuthHelper { else { yield fs.promises.writeFile(newGitConfigPath, ''); } + // Override HOME + core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`); + this.git.setEnvironmentVariable('HOME', this.temporaryHomePath); + // Setup the workspace as a safe directory, so if we pass this into a container job with a different user it doesn't fail + // Otherwise all git commands we run in a container fail + core.info(`Adding working directory to the temporary git global config as a safe directory`); + yield this.git + .config('safe.directory', repositoryPath !== null && repositoryPath !== void 0 ? repositoryPath : this.settings.repositoryPath, true, true) + .catch(error => { + core.info(`Failed to initialize safe directory with error: ${error}`); + }); + return newGitConfigPath; + }); + } + configureGlobalAuth() { + return __awaiter(this, void 0, void 0, function* () { + // 'configureTempGlobalConfig' noops if already set, just returns the path + const newGitConfigPath = yield this.configureTempGlobalConfig(); try { - // Override HOME - core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`); - this.git.setEnvironmentVariable('HOME', this.temporaryHomePath); // Configure the token yield this.configureToken(newGitConfigPath, true); // Configure HTTPS instead of SSH @@ -6657,11 +6676,14 @@ class GitAuthHelper { yield this.removeToken(); }); } - removeGlobalAuth() { + removeGlobalConfig() { + var _a; return __awaiter(this, void 0, void 0, function* () { - core.debug(`Unsetting HOME override`); - this.git.removeEnvironmentVariable('HOME'); - yield io.rmRF(this.temporaryHomePath); + if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) { + core.debug(`Unsetting HOME override`); + this.git.removeEnvironmentVariable('HOME'); + yield io.rmRF(this.temporaryHomePath); + } }); } configureSsh() { @@ -7326,40 +7348,48 @@ function getSource(settings) { core.startGroup('Getting Git version info'); const git = yield getGitCommandManager(settings); core.endGroup(); - // Prepare existing directory, otherwise recreate - if (isExisting) { - yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref); - } - if (!git) { - // Downloading using REST API - core.info(`The repository will be downloaded using the GitHub REST API`); - core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`); - if (settings.submodules) { - throw new Error(`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`); - } - else if (settings.sshKey) { - throw new Error(`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`); - } - yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath); - return; - } - // Save state for POST action - stateHelper.setRepositoryPath(settings.repositoryPath); - // Initialize the repository - if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) { - core.startGroup('Initializing the repository'); - yield git.init(); - yield git.remoteAdd('origin', repositoryUrl); - core.endGroup(); - } - // Disable automatic garbage collection - core.startGroup('Disabling automatic garbage collection'); - if (!(yield git.tryDisableAutomaticGarbageCollection())) { - core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`); - } - core.endGroup(); - const authHelper = gitAuthHelper.createAuthHelper(git, settings); + let authHelper = null; try { + if (git) { + authHelper = gitAuthHelper.createAuthHelper(git, settings); + yield authHelper.configureTempGlobalConfig(); + } + // Prepare existing directory, otherwise recreate + if (isExisting) { + yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref); + } + if (!git) { + // Downloading using REST API + core.info(`The repository will be downloaded using the GitHub REST API`); + core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`); + if (settings.submodules) { + throw new Error(`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`); + } + else if (settings.sshKey) { + throw new Error(`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`); + } + yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath); + return; + } + // Save state for POST action + stateHelper.setRepositoryPath(settings.repositoryPath); + // Initialize the repository + if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) { + core.startGroup('Initializing the repository'); + yield git.init(); + yield git.remoteAdd('origin', repositoryUrl); + core.endGroup(); + } + // Disable automatic garbage collection + core.startGroup('Disabling automatic garbage collection'); + if (!(yield git.tryDisableAutomaticGarbageCollection())) { + core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`); + } + core.endGroup(); + // If we didn't initialize it above, do it now + if (!authHelper) { + authHelper = gitAuthHelper.createAuthHelper(git, settings); + } // Configure auth core.startGroup('Setting up auth'); yield authHelper.configureAuth(); @@ -7415,27 +7445,21 @@ function getSource(settings) { core.endGroup(); // Submodules if (settings.submodules) { - try { - // Temporarily override global config - core.startGroup('Setting up auth for fetching submodules'); - yield authHelper.configureGlobalAuth(); + // Temporarily override global config + core.startGroup('Setting up auth for fetching submodules'); + yield authHelper.configureGlobalAuth(); + core.endGroup(); + // Checkout submodules + core.startGroup('Fetching submodules'); + yield git.submoduleSync(settings.nestedSubmodules); + yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules); + yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules); + core.endGroup(); + // Persist credentials + if (settings.persistCredentials) { + core.startGroup('Persisting credentials for submodules'); + yield authHelper.configureSubmoduleAuth(); core.endGroup(); - // Checkout submodules - core.startGroup('Fetching submodules'); - yield git.submoduleSync(settings.nestedSubmodules); - yield git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules); - yield git.submoduleForeach('git config --local gc.auto 0', settings.nestedSubmodules); - core.endGroup(); - // Persist credentials - if (settings.persistCredentials) { - core.startGroup('Persisting credentials for submodules'); - yield authHelper.configureSubmoduleAuth(); - core.endGroup(); - } - } - finally { - // Remove temporary global config override - yield authHelper.removeGlobalAuth(); } } // Get commit information @@ -7447,10 +7471,13 @@ function getSource(settings) { } finally { // Remove auth - if (!settings.persistCredentials) { - core.startGroup('Removing auth'); - yield authHelper.removeAuth(); - core.endGroup(); + if (authHelper) { + if (!settings.persistCredentials) { + core.startGroup('Removing auth'); + yield authHelper.removeAuth(); + core.endGroup(); + } + authHelper.removeGlobalConfig(); } } }); @@ -7472,7 +7499,13 @@ function cleanup(repositoryPath) { } // Remove auth const authHelper = gitAuthHelper.createAuthHelper(git); - yield authHelper.removeAuth(); + try { + yield authHelper.configureTempGlobalConfig(repositoryPath); + yield authHelper.removeAuth(); + } + finally { + yield authHelper.removeGlobalConfig(); + } }); } exports.cleanup = cleanup; diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 233b3e6..385142a 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -19,8 +19,9 @@ export interface IGitAuthHelper { configureAuth(): Promise configureGlobalAuth(): Promise configureSubmoduleAuth(): Promise + configureTempGlobalConfig(repositoryPath?: string): Promise removeAuth(): Promise - removeGlobalAuth(): Promise + removeGlobalConfig(): Promise } export function createAuthHelper( @@ -80,7 +81,11 @@ class GitAuthHelper { await this.configureToken() } - async configureGlobalAuth(): Promise { + async configureTempGlobalConfig(repositoryPath?: string): Promise { + // Already setup global config + if (this.temporaryHomePath?.length > 0) { + return path.join(this.temporaryHomePath, '.gitconfig') + } // Create a temp home directory const runnerTemp = process.env['RUNNER_TEMP'] || '' assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') @@ -110,13 +115,34 @@ class GitAuthHelper { await fs.promises.writeFile(newGitConfigPath, '') } - try { - // Override HOME - core.info( - `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes` - ) - this.git.setEnvironmentVariable('HOME', this.temporaryHomePath) + // Override HOME + core.info( + `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes` + ) + this.git.setEnvironmentVariable('HOME', this.temporaryHomePath) + // Setup the workspace as a safe directory, so if we pass this into a container job with a different user it doesn't fail + // Otherwise all git commands we run in a container fail + core.info( + `Adding working directory to the temporary git global config as a safe directory` + ) + await this.git + .config( + 'safe.directory', + repositoryPath ?? this.settings.repositoryPath, + true, + true + ) + .catch(error => { + core.info(`Failed to initialize safe directory with error: ${error}`) + }) + return newGitConfigPath + } + + async configureGlobalAuth(): Promise { + // 'configureTempGlobalConfig' noops if already set, just returns the path + const newGitConfigPath = await this.configureTempGlobalConfig() + try { // Configure the token await this.configureToken(newGitConfigPath, true) @@ -181,10 +207,12 @@ class GitAuthHelper { await this.removeToken() } - async removeGlobalAuth(): Promise { - core.debug(`Unsetting HOME override`) - this.git.removeEnvironmentVariable('HOME') - await io.rmRF(this.temporaryHomePath) + async removeGlobalConfig(): Promise { + if (this.temporaryHomePath?.length > 0) { + core.debug(`Unsetting HOME override`) + this.git.removeEnvironmentVariable('HOME') + await io.rmRF(this.temporaryHomePath) + } } private async configureSsh(): Promise { diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 42a12e0..0913229 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -36,68 +36,77 @@ export async function getSource(settings: IGitSourceSettings): Promise { const git = await getGitCommandManager(settings) core.endGroup() - // Prepare existing directory, otherwise recreate - if (isExisting) { - await gitDirectoryHelper.prepareExistingDirectory( - git, - settings.repositoryPath, - repositoryUrl, - settings.clean, - settings.ref - ) - } + let authHelper: gitAuthHelper.IGitAuthHelper | null = null + try { + if (git) { + authHelper = gitAuthHelper.createAuthHelper(git, settings) + await authHelper.configureTempGlobalConfig() + } - if (!git) { - // Downloading using REST API - core.info(`The repository will be downloaded using the GitHub REST API`) - core.info( - `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` - ) - if (settings.submodules) { - throw new Error( - `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` - ) - } else if (settings.sshKey) { - throw new Error( - `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` + // Prepare existing directory, otherwise recreate + if (isExisting) { + await gitDirectoryHelper.prepareExistingDirectory( + git, + settings.repositoryPath, + repositoryUrl, + settings.clean, + settings.ref ) } - await githubApiHelper.downloadRepository( - settings.authToken, - settings.repositoryOwner, - settings.repositoryName, - settings.ref, - settings.commit, - settings.repositoryPath - ) - return - } + if (!git) { + // Downloading using REST API + core.info(`The repository will be downloaded using the GitHub REST API`) + core.info( + `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` + ) + if (settings.submodules) { + throw new Error( + `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` + ) + } else if (settings.sshKey) { + throw new Error( + `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` + ) + } - // Save state for POST action - stateHelper.setRepositoryPath(settings.repositoryPath) + await githubApiHelper.downloadRepository( + settings.authToken, + settings.repositoryOwner, + settings.repositoryName, + settings.ref, + settings.commit, + settings.repositoryPath + ) + return + } - // Initialize the repository - if ( - !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) - ) { - core.startGroup('Initializing the repository') - await git.init() - await git.remoteAdd('origin', repositoryUrl) + // Save state for POST action + stateHelper.setRepositoryPath(settings.repositoryPath) + + // Initialize the repository + if ( + !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) + ) { + core.startGroup('Initializing the repository') + await git.init() + await git.remoteAdd('origin', repositoryUrl) + core.endGroup() + } + + // Disable automatic garbage collection + core.startGroup('Disabling automatic garbage collection') + if (!(await git.tryDisableAutomaticGarbageCollection())) { + core.warning( + `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.` + ) + } core.endGroup() - } - // Disable automatic garbage collection - core.startGroup('Disabling automatic garbage collection') - if (!(await git.tryDisableAutomaticGarbageCollection())) { - core.warning( - `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.` - ) - } - core.endGroup() - - const authHelper = gitAuthHelper.createAuthHelper(git, settings) - try { + // If we didn't initialize it above, do it now + if (!authHelper) { + authHelper = gitAuthHelper.createAuthHelper(git, settings) + } // Configure auth core.startGroup('Setting up auth') await authHelper.configureAuth() @@ -170,34 +179,26 @@ export async function getSource(settings: IGitSourceSettings): Promise { // Submodules if (settings.submodules) { - try { - // Temporarily override global config - core.startGroup('Setting up auth for fetching submodules') - await authHelper.configureGlobalAuth() - core.endGroup() + // Temporarily override global config + core.startGroup('Setting up auth for fetching submodules') + await authHelper.configureGlobalAuth() + core.endGroup() - // Checkout submodules - core.startGroup('Fetching submodules') - await git.submoduleSync(settings.nestedSubmodules) - await git.submoduleUpdate( - settings.fetchDepth, - settings.nestedSubmodules - ) - await git.submoduleForeach( - 'git config --local gc.auto 0', - settings.nestedSubmodules - ) - core.endGroup() + // Checkout submodules + core.startGroup('Fetching submodules') + await git.submoduleSync(settings.nestedSubmodules) + await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules) + await git.submoduleForeach( + 'git config --local gc.auto 0', + settings.nestedSubmodules + ) + core.endGroup() - // Persist credentials - if (settings.persistCredentials) { - core.startGroup('Persisting credentials for submodules') - await authHelper.configureSubmoduleAuth() - core.endGroup() - } - } finally { - // Remove temporary global config override - await authHelper.removeGlobalAuth() + // Persist credentials + if (settings.persistCredentials) { + core.startGroup('Persisting credentials for submodules') + await authHelper.configureSubmoduleAuth() + core.endGroup() } } @@ -218,10 +219,13 @@ export async function getSource(settings: IGitSourceSettings): Promise { ) } finally { // Remove auth - if (!settings.persistCredentials) { - core.startGroup('Removing auth') - await authHelper.removeAuth() - core.endGroup() + if (authHelper) { + if (!settings.persistCredentials) { + core.startGroup('Removing auth') + await authHelper.removeAuth() + core.endGroup() + } + authHelper.removeGlobalConfig() } } } @@ -244,7 +248,12 @@ export async function cleanup(repositoryPath: string): Promise { // Remove auth const authHelper = gitAuthHelper.createAuthHelper(git) - await authHelper.removeAuth() + try { + await authHelper.configureTempGlobalConfig(repositoryPath) + await authHelper.removeAuth() + } finally { + await authHelper.removeGlobalConfig() + } } async function getGitCommandManager(