0
1
Fork 0

persist core.sshCommand for submodules (#184)

* persist core.sshCommand for submodules

* update verbiage; add comments

* fail when submodules or ssh-key and fallback to REST API
This commit is contained in:
eric sciple 2020-03-12 11:42:38 -04:00 committed by GitHub
parent b2e6b7ed13
commit 9a3a9ade82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 119 deletions

View File

@ -49,19 +49,19 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# with the local git config, which enables your scripts to run authenticated git # with the local git config, which enables your scripts to run authenticated git
# commands. The post-job step removes the PAT. # commands. The post-job step removes the PAT.
# #
# We recommend creating a service account with the least permissions necessary. # We recommend using a service account with the least permissions necessary. Also
# Also when generating a new PAT, select the least scopes necessary. # when generating a new PAT, select the least scopes necessary.
# #
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
# #
# Default: ${{ github.token }} # Default: ${{ github.token }}
token: '' token: ''
# SSH key used to fetch the repository. SSH key is configured with the local git # SSH key used to fetch the repository. The SSH key is configured with the local
# config, which enables your scripts to run authenticated git commands. The # git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key. # post-job step removes the SSH key.
# #
# We recommend creating a service account with the least permissions necessary. # We recommend using a service account with the least permissions necessary.
# #
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-key: '' ssh-key: ''

View File

@ -320,6 +320,8 @@ describe('git-auth-helper tests', () => {
).toString() ).toString()
expect(actualSshKeyContent).toBe(settings.sshKey + '\n') expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
if (!isWindows) { if (!isWindows) {
// Assert read/write for user, not group or others.
// Otherwise SSH client will error.
expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe( expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
0o600 0o600
) )
@ -437,14 +439,74 @@ describe('git-auth-helper tests', () => {
} }
) )
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet = const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
'configureSubmoduleAuth configures token when persist credentials true and SSH key not set' 'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
it( it(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet, configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
async () => { async () => {
// Arrange // Arrange
await setup( await setup(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
)
settings.persistCredentials = false
settings.sshKey = ''
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toBeCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
/unset-all.*insteadOf/
)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
) )
settings.sshKey = '' settings.sshKey = ''
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
@ -465,21 +527,21 @@ describe('git-auth-helper tests', () => {
} }
) )
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet = const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
'configureSubmoduleAuth configures token when persist credentials true and SSH key set' 'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
it( it(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet, configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
async () => { async () => {
if (!sshPath) { if (!sshPath) {
process.stdout.write( process.stdout.write(
`Skipped test "${configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n` `Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
) )
return return
} }
// Arrange // Arrange
await setup( await setup(
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
) )
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth() await authHelper.configureAuth()
@ -490,96 +552,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth() await authHelper.configureSubmoduleAuth()
// Assert // Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2) expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
} expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
)
const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse =
'configureSubmoduleAuth does not configure token when persist credentials false'
it(
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse
)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toBeCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
/unset-all.*insteadOf/
)
}
)
const configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet =
'configureSubmoduleAuth does not configure URL insteadOf when persist credentials true and SSH key set'
it(
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet,
async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet
)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
}
)
const configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse =
'configureSubmoduleAuth removes URL insteadOf when persist credentials false'
it(
configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse
)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toBeCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
/unset-all.*insteadOf/
)
} }
) )

View File

@ -16,7 +16,7 @@ inputs:
commands. The post-job step removes the PAT. commands. The post-job step removes the PAT.
We recommend creating a service account with the least permissions necessary. We recommend using a service account with the least permissions necessary.
Also when generating a new PAT, select the least scopes necessary. Also when generating a new PAT, select the least scopes necessary.
@ -24,12 +24,12 @@ inputs:
default: ${{ github.token }} default: ${{ github.token }}
ssh-key: ssh-key:
description: > description: >
SSH key used to fetch the repository. SSH key is configured with the local SSH key used to fetch the repository. The SSH key is configured with the local
git config, which enables your scripts to run authenticated git commands. git config, which enables your scripts to run authenticated git commands.
The post-job step removes the SSH key. The post-job step removes the SSH key.
We recommend creating a service account with the least permissions necessary. We recommend using a service account with the least permissions necessary.
[Learn more about creating and using [Learn more about creating and using

27
dist/index.js vendored
View File

@ -5122,6 +5122,7 @@ class GitAuthHelper {
this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`; this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`;
this.insteadOfKey = `url.https://${HOSTNAME}/.insteadOf`; this.insteadOfKey = `url.https://${HOSTNAME}/.insteadOf`;
this.insteadOfValue = `git@${HOSTNAME}:`; this.insteadOfValue = `git@${HOSTNAME}:`;
this.sshCommand = '';
this.sshKeyPath = ''; this.sshKeyPath = '';
this.sshKnownHostsPath = ''; this.sshKnownHostsPath = '';
this.temporaryHomePath = ''; this.temporaryHomePath = '';
@ -5205,8 +5206,12 @@ class GitAuthHelper {
core.debug(`Replacing token placeholder in '${configPath}'`); core.debug(`Replacing token placeholder in '${configPath}'`);
this.replaceTokenPlaceholder(configPath); this.replaceTokenPlaceholder(configPath);
} }
// Configure HTTPS instead of SSH if (this.settings.sshKey) {
if (!this.settings.sshKey) { // Configure core.sshCommand
yield this.git.submoduleForeach(`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`, this.settings.nestedSubmodules);
}
else {
// Configure HTTPS instead of SSH
yield this.git.submoduleForeach(`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, this.settings.nestedSubmodules); yield this.git.submoduleForeach(`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, this.settings.nestedSubmodules);
} }
} }
@ -5268,16 +5273,16 @@ class GitAuthHelper {
yield fs.promises.writeFile(this.sshKnownHostsPath, knownHosts); yield fs.promises.writeFile(this.sshKnownHostsPath, knownHosts);
// Configure GIT_SSH_COMMAND // Configure GIT_SSH_COMMAND
const sshPath = yield io.which('ssh', true); const sshPath = yield io.which('ssh', true);
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`; this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`;
if (this.settings.sshStrict) { if (this.settings.sshStrict) {
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'; this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no';
} }
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`; this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`;
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`); core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`);
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand); this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand);
// Configure core.sshCommand // Configure core.sshCommand
if (this.settings.persistCredentials) { if (this.settings.persistCredentials) {
yield this.git.config(SSH_COMMAND_KEY, sshCommand); yield this.git.config(SSH_COMMAND_KEY, this.sshCommand);
} }
}); });
} }
@ -5820,6 +5825,12 @@ function getSource(settings) {
// Downloading using REST API // Downloading using REST API
core.info(`The repository will be downloaded using the GitHub 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`); 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); yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
return; return;
} }

View File

@ -37,6 +37,7 @@ class GitAuthHelper {
private readonly tokenPlaceholderConfigValue: string private readonly tokenPlaceholderConfigValue: string
private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf` private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf`
private readonly insteadOfValue: string = `git@${HOSTNAME}:` private readonly insteadOfValue: string = `git@${HOSTNAME}:`
private sshCommand = ''
private sshKeyPath = '' private sshKeyPath = ''
private sshKnownHostsPath = '' private sshKnownHostsPath = ''
private temporaryHomePath = '' private temporaryHomePath = ''
@ -144,8 +145,14 @@ class GitAuthHelper {
this.replaceTokenPlaceholder(configPath) this.replaceTokenPlaceholder(configPath)
} }
// Configure HTTPS instead of SSH if (this.settings.sshKey) {
if (!this.settings.sshKey) { // Configure core.sshCommand
await this.git.submoduleForeach(
`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`,
this.settings.nestedSubmodules
)
} else {
// Configure HTTPS instead of SSH
await this.git.submoduleForeach( await this.git.submoduleForeach(
`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, `git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`,
this.settings.nestedSubmodules this.settings.nestedSubmodules
@ -218,21 +225,21 @@ class GitAuthHelper {
// Configure GIT_SSH_COMMAND // Configure GIT_SSH_COMMAND
const sshPath = await io.which('ssh', true) const sshPath = await io.which('ssh', true)
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename( this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
this.sshKeyPath this.sshKeyPath
)}"` )}"`
if (this.settings.sshStrict) { if (this.settings.sshStrict) {
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no' this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
} }
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename( this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
this.sshKnownHostsPath this.sshKnownHostsPath
)}"` )}"`
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`) core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand) this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
// Configure core.sshCommand // Configure core.sshCommand
if (this.settings.persistCredentials) { if (this.settings.persistCredentials) {
await this.git.config(SSH_COMMAND_KEY, sshCommand) await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
} }
} }

View File

@ -57,6 +57,16 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
core.info( core.info(
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` `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.`
)
}
await githubApiHelper.downloadRepository( await githubApiHelper.downloadRepository(
settings.authToken, settings.authToken,
settings.repositoryOwner, settings.repositoryOwner,