Skip to content

Commit 9a3a9ad

Browse files
authored
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
1 parent b2e6b7e commit 9a3a9ad

6 files changed

+84
-78
lines changed

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,19 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
4949
# with the local git config, which enables your scripts to run authenticated git
5050
# commands. The post-job step removes the PAT.
5151
#
52-
# We recommend creating a service account with the least permissions necessary.
53-
# Also when generating a new PAT, select the least scopes necessary.
52+
# We recommend using a service account with the least permissions necessary. Also
53+
# when generating a new PAT, select the least scopes necessary.
5454
#
5555
# [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)
5656
#
5757
# Default: ${{ github.token }}
5858
token: ''
5959

60-
# SSH key used to fetch the repository. SSH key is configured with the local git
61-
# config, which enables your scripts to run authenticated git commands. The
60+
# SSH key used to fetch the repository. The SSH key is configured with the local
61+
# git config, which enables your scripts to run authenticated git commands. The
6262
# post-job step removes the SSH key.
6363
#
64-
# We recommend creating a service account with the least permissions necessary.
64+
# We recommend using a service account with the least permissions necessary.
6565
#
6666
# [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)
6767
ssh-key: ''

__test__/git-auth-helper.test.ts

+32-54
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ describe('git-auth-helper tests', () => {
320320
).toString()
321321
expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
322322
if (!isWindows) {
323+
// Assert read/write for user, not group or others.
324+
// Otherwise SSH client will error.
323325
expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
324326
0o600
325327
)
@@ -437,15 +439,16 @@ describe('git-auth-helper tests', () => {
437439
}
438440
)
439441

440-
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet =
441-
'configureSubmoduleAuth configures token when persist credentials true and SSH key not set'
442+
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
443+
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
442444
it(
443-
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet,
445+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
444446
async () => {
445447
// Arrange
446448
await setup(
447-
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet
449+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
448450
)
451+
settings.persistCredentials = false
449452
settings.sshKey = ''
450453
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
451454
await authHelper.configureAuth()
@@ -456,31 +459,30 @@ describe('git-auth-helper tests', () => {
456459
await authHelper.configureSubmoduleAuth()
457460

458461
// Assert
459-
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
460-
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
462+
expect(mockSubmoduleForeach).toBeCalledTimes(1)
463+
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
461464
/unset-all.*insteadOf/
462465
)
463-
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
464-
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/url.*insteadOf/)
465466
}
466467
)
467468

468-
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet =
469-
'configureSubmoduleAuth configures token when persist credentials true and SSH key set'
469+
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
470+
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
470471
it(
471-
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet,
472+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
472473
async () => {
473474
if (!sshPath) {
474475
process.stdout.write(
475-
`Skipped test "${configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
476+
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
476477
)
477478
return
478479
}
479480

480481
// Arrange
481482
await setup(
482-
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet
483+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
483484
)
485+
settings.persistCredentials = false
484486
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
485487
await authHelper.configureAuth()
486488
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
@@ -490,24 +492,23 @@ describe('git-auth-helper tests', () => {
490492
await authHelper.configureSubmoduleAuth()
491493

492494
// Assert
493-
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
495+
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
494496
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
495497
/unset-all.*insteadOf/
496498
)
497-
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
498499
}
499500
)
500501

501-
const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse =
502-
'configureSubmoduleAuth does not configure token when persist credentials false'
502+
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
503+
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
503504
it(
504-
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse,
505+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
505506
async () => {
506507
// Arrange
507508
await setup(
508-
configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse
509+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
509510
)
510-
settings.persistCredentials = false
511+
settings.sshKey = ''
511512
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
512513
await authHelper.configureAuth()
513514
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
@@ -517,28 +518,30 @@ describe('git-auth-helper tests', () => {
517518
await authHelper.configureSubmoduleAuth()
518519

519520
// Assert
520-
expect(mockSubmoduleForeach).toBeCalledTimes(1)
521-
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
521+
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
522+
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
522523
/unset-all.*insteadOf/
523524
)
525+
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
526+
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/url.*insteadOf/)
524527
}
525528
)
526529

527-
const configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet =
528-
'configureSubmoduleAuth does not configure URL insteadOf when persist credentials true and SSH key set'
530+
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
531+
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
529532
it(
530-
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet,
533+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
531534
async () => {
532535
if (!sshPath) {
533536
process.stdout.write(
534-
`Skipped test "${configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
537+
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
535538
)
536539
return
537540
}
538541

539542
// Arrange
540543
await setup(
541-
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet
544+
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
542545
)
543546
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
544547
await authHelper.configureAuth()
@@ -549,37 +552,12 @@ describe('git-auth-helper tests', () => {
549552
await authHelper.configureSubmoduleAuth()
550553

551554
// Assert
552-
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
555+
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
553556
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
554557
/unset-all.*insteadOf/
555558
)
556559
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
557-
}
558-
)
559-
560-
const configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse =
561-
'configureSubmoduleAuth removes URL insteadOf when persist credentials false'
562-
it(
563-
configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse,
564-
async () => {
565-
// Arrange
566-
await setup(
567-
configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse
568-
)
569-
settings.persistCredentials = false
570-
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
571-
await authHelper.configureAuth()
572-
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
573-
mockSubmoduleForeach.mockClear() // reset calls
574-
575-
// Act
576-
await authHelper.configureSubmoduleAuth()
577-
578-
// Assert
579-
expect(mockSubmoduleForeach).toBeCalledTimes(1)
580-
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
581-
/unset-all.*insteadOf/
582-
)
560+
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
583561
}
584562
)
585563

action.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ inputs:
1616
commands. The post-job step removes the PAT.
1717
1818
19-
We recommend creating a service account with the least permissions necessary.
19+
We recommend using a service account with the least permissions necessary.
2020
Also when generating a new PAT, select the least scopes necessary.
2121
2222
2323
[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)
2424
default: ${{ github.token }}
2525
ssh-key:
2626
description: >
27-
SSH key used to fetch the repository. SSH key is configured with the local
27+
SSH key used to fetch the repository. The SSH key is configured with the local
2828
git config, which enables your scripts to run authenticated git commands.
2929
The post-job step removes the SSH key.
3030
3131
32-
We recommend creating a service account with the least permissions necessary.
32+
We recommend using a service account with the least permissions necessary.
3333
3434
3535
[Learn more about creating and using

dist/index.js

+19-8
Original file line numberDiff line numberDiff line change
@@ -5122,6 +5122,7 @@ class GitAuthHelper {
51225122
this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`;
51235123
this.insteadOfKey = `url.https://${HOSTNAME}/.insteadOf`;
51245124
this.insteadOfValue = `git@${HOSTNAME}:`;
5125+
this.sshCommand = '';
51255126
this.sshKeyPath = '';
51265127
this.sshKnownHostsPath = '';
51275128
this.temporaryHomePath = '';
@@ -5205,8 +5206,12 @@ class GitAuthHelper {
52055206
core.debug(`Replacing token placeholder in '${configPath}'`);
52065207
this.replaceTokenPlaceholder(configPath);
52075208
}
5208-
// Configure HTTPS instead of SSH
5209-
if (!this.settings.sshKey) {
5209+
if (this.settings.sshKey) {
5210+
// Configure core.sshCommand
5211+
yield this.git.submoduleForeach(`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`, this.settings.nestedSubmodules);
5212+
}
5213+
else {
5214+
// Configure HTTPS instead of SSH
52105215
yield this.git.submoduleForeach(`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, this.settings.nestedSubmodules);
52115216
}
52125217
}
@@ -5268,16 +5273,16 @@ class GitAuthHelper {
52685273
yield fs.promises.writeFile(this.sshKnownHostsPath, knownHosts);
52695274
// Configure GIT_SSH_COMMAND
52705275
const sshPath = yield io.which('ssh', true);
5271-
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`;
5276+
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`;
52725277
if (this.settings.sshStrict) {
5273-
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no';
5278+
this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no';
52745279
}
5275-
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`;
5276-
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`);
5277-
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand);
5280+
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`;
5281+
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`);
5282+
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand);
52785283
// Configure core.sshCommand
52795284
if (this.settings.persistCredentials) {
5280-
yield this.git.config(SSH_COMMAND_KEY, sshCommand);
5285+
yield this.git.config(SSH_COMMAND_KEY, this.sshCommand);
52815286
}
52825287
});
52835288
}
@@ -5820,6 +5825,12 @@ function getSource(settings) {
58205825
// Downloading using REST API
58215826
core.info(`The repository will be downloaded using the GitHub REST API`);
58225827
core.info(`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`);
5828+
if (settings.submodules) {
5829+
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.`);
5830+
}
5831+
else if (settings.sshKey) {
5832+
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.`);
5833+
}
58235834
yield githubApiHelper.downloadRepository(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit, settings.repositoryPath);
58245835
return;
58255836
}

src/git-auth-helper.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class GitAuthHelper {
3737
private readonly tokenPlaceholderConfigValue: string
3838
private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf`
3939
private readonly insteadOfValue: string = `git@${HOSTNAME}:`
40+
private sshCommand = ''
4041
private sshKeyPath = ''
4142
private sshKnownHostsPath = ''
4243
private temporaryHomePath = ''
@@ -144,8 +145,14 @@ class GitAuthHelper {
144145
this.replaceTokenPlaceholder(configPath)
145146
}
146147

147-
// Configure HTTPS instead of SSH
148-
if (!this.settings.sshKey) {
148+
if (this.settings.sshKey) {
149+
// Configure core.sshCommand
150+
await this.git.submoduleForeach(
151+
`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`,
152+
this.settings.nestedSubmodules
153+
)
154+
} else {
155+
// Configure HTTPS instead of SSH
149156
await this.git.submoduleForeach(
150157
`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`,
151158
this.settings.nestedSubmodules
@@ -218,21 +225,21 @@ class GitAuthHelper {
218225

219226
// Configure GIT_SSH_COMMAND
220227
const sshPath = await io.which('ssh', true)
221-
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
228+
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
222229
this.sshKeyPath
223230
)}"`
224231
if (this.settings.sshStrict) {
225-
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
232+
this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
226233
}
227-
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
234+
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
228235
this.sshKnownHostsPath
229236
)}"`
230-
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`)
231-
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand)
237+
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
238+
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
232239

233240
// Configure core.sshCommand
234241
if (this.settings.persistCredentials) {
235-
await this.git.config(SSH_COMMAND_KEY, sshCommand)
242+
await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
236243
}
237244
}
238245

src/git-source-provider.ts

+10
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
5757
core.info(
5858
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
5959
)
60+
if (settings.submodules) {
61+
throw new Error(
62+
`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.`
63+
)
64+
} else if (settings.sshKey) {
65+
throw new Error(
66+
`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.`
67+
)
68+
}
69+
6070
await githubApiHelper.downloadRepository(
6171
settings.authToken,
6272
settings.repositoryOwner,

0 commit comments

Comments
 (0)