add ssh support (#163)
parent
80602fafba
commit
b2e6b7ed13
40
README.md
40
README.md
|
@ -45,14 +45,40 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
|
||||||
# Otherwise, defaults to `master`.
|
# Otherwise, defaults to `master`.
|
||||||
ref: ''
|
ref: ''
|
||||||
|
|
||||||
# Auth token used to fetch the repository. The token is stored in the local git
|
# Personal access token (PAT) used to fetch the repository. The PAT is configured
|
||||||
# config, which enables your scripts to run authenticated git commands. The
|
# with the local git config, which enables your scripts to run authenticated git
|
||||||
# post-job step removes the token from the git config. [Learn more about creating
|
# commands. The post-job step removes the PAT.
|
||||||
# and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
#
|
||||||
|
# We recommend creating a service account with the least permissions necessary.
|
||||||
|
# Also 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)
|
||||||
|
#
|
||||||
# Default: ${{ github.token }}
|
# Default: ${{ github.token }}
|
||||||
token: ''
|
token: ''
|
||||||
|
|
||||||
# Whether to persist the token in the git config
|
# SSH key used to fetch the repository. SSH key is configured with the local git
|
||||||
|
# config, which enables your scripts to run authenticated git commands. The
|
||||||
|
# post-job step removes the SSH key.
|
||||||
|
#
|
||||||
|
# We recommend creating 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)
|
||||||
|
ssh-key: ''
|
||||||
|
|
||||||
|
# Known hosts in addition to the user and global host key database. The public SSH
|
||||||
|
# keys for a host may be obtained using the utility `ssh-keyscan`. For example,
|
||||||
|
# `ssh-keyscan github.com`. The public key for github.com is always implicitly
|
||||||
|
# added.
|
||||||
|
ssh-known-hosts: ''
|
||||||
|
|
||||||
|
# Whether to perform strict host key checking. When true, adds the options
|
||||||
|
# `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use
|
||||||
|
# the input `ssh-known-hosts` to configure additional hosts.
|
||||||
|
# Default: true
|
||||||
|
ssh-strict: ''
|
||||||
|
|
||||||
|
# Whether to configure the token or SSH key with the local git config
|
||||||
# Default: true
|
# Default: true
|
||||||
persist-credentials: ''
|
persist-credentials: ''
|
||||||
|
|
||||||
|
@ -73,6 +99,10 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
|
||||||
|
|
||||||
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
||||||
# recursively checkout submodules.
|
# recursively checkout submodules.
|
||||||
|
#
|
||||||
|
# When the `ssh-key` input is not provided, SSH URLs beginning with
|
||||||
|
# `git@github.com:` are converted to HTTPS.
|
||||||
|
#
|
||||||
# Default: false
|
# Default: false
|
||||||
submodules: ''
|
submodules: ''
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,10 +2,13 @@ import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as gitAuthHelper from '../lib/git-auth-helper'
|
import * as gitAuthHelper from '../lib/git-auth-helper'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import * as stateHelper from '../lib/state-helper'
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
import {IGitSourceSettings} from '../lib/git-source-settings'
|
||||||
|
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
||||||
const originalRunnerTemp = process.env['RUNNER_TEMP']
|
const originalRunnerTemp = process.env['RUNNER_TEMP']
|
||||||
const originalHome = process.env['HOME']
|
const originalHome = process.env['HOME']
|
||||||
|
@ -16,9 +19,13 @@ let runnerTemp: string
|
||||||
let tempHomedir: string
|
let tempHomedir: string
|
||||||
let git: IGitCommandManager & {env: {[key: string]: string}}
|
let git: IGitCommandManager & {env: {[key: string]: string}}
|
||||||
let settings: IGitSourceSettings
|
let settings: IGitSourceSettings
|
||||||
|
let sshPath: string
|
||||||
|
|
||||||
describe('git-auth-helper tests', () => {
|
describe('git-auth-helper tests', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
// SSH
|
||||||
|
sshPath = await io.which('ssh')
|
||||||
|
|
||||||
// Clear test workspace
|
// Clear test workspace
|
||||||
await io.rmRF(testWorkspace)
|
await io.rmRF(testWorkspace)
|
||||||
})
|
})
|
||||||
|
@ -32,6 +39,12 @@ describe('git-auth-helper tests', () => {
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||||
|
|
||||||
|
// Mock state helper
|
||||||
|
jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
|
||||||
|
jest
|
||||||
|
.spyOn(stateHelper, 'setSshKnownHostsPath')
|
||||||
|
.mockImplementation(jest.fn())
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -108,6 +121,52 @@ describe('git-auth-helper tests', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const configureAuth_copiesUserKnownHosts =
|
||||||
|
'configureAuth copies user known hosts'
|
||||||
|
it(configureAuth_copiesUserKnownHosts, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arange
|
||||||
|
await setup(configureAuth_copiesUserKnownHosts)
|
||||||
|
expect(settings.sshKey).toBeTruthy() // sanity check
|
||||||
|
|
||||||
|
// Mock fs.promises.readFile
|
||||||
|
const realReadFile = fs.promises.readFile
|
||||||
|
jest.spyOn(fs.promises, 'readFile').mockImplementation(
|
||||||
|
async (file: any, options: any): Promise<Buffer> => {
|
||||||
|
const userKnownHostsPath = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
'.ssh',
|
||||||
|
'known_hosts'
|
||||||
|
)
|
||||||
|
if (file === userKnownHostsPath) {
|
||||||
|
return Buffer.from('some-domain.com ssh-rsa ABCDEF')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await realReadFile(file, options)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert known hosts
|
||||||
|
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
const actualSshKnownHostsContent = (
|
||||||
|
await fs.promises.readFile(actualSshKnownHostsPath)
|
||||||
|
).toString()
|
||||||
|
expect(actualSshKnownHostsContent).toMatch(
|
||||||
|
/some-domain\.com ssh-rsa ABCDEF/
|
||||||
|
)
|
||||||
|
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
||||||
|
})
|
||||||
|
|
||||||
const configureAuth_registersBasicCredentialAsSecret =
|
const configureAuth_registersBasicCredentialAsSecret =
|
||||||
'configureAuth registers basic credential as secret'
|
'configureAuth registers basic credential as secret'
|
||||||
it(configureAuth_registersBasicCredentialAsSecret, async () => {
|
it(configureAuth_registersBasicCredentialAsSecret, async () => {
|
||||||
|
@ -129,6 +188,173 @@ describe('git-auth-helper tests', () => {
|
||||||
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
|
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setsSshCommandEnvVarWhenPersistCredentialsFalse =
|
||||||
|
'sets SSH command env var when persist-credentials false'
|
||||||
|
it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse)
|
||||||
|
settings.persistCredentials = false
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert git env var
|
||||||
|
const actualKeyPath = await getActualSshKeyPath()
|
||||||
|
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
||||||
|
actualKeyPath
|
||||||
|
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
||||||
|
actualKnownHostsPath
|
||||||
|
)}"`
|
||||||
|
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
|
||||||
|
'GIT_SSH_COMMAND',
|
||||||
|
expectedSshCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
// Asserty git config
|
||||||
|
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x)
|
||||||
|
expect(gitConfigLines).toHaveLength(1)
|
||||||
|
expect(gitConfigLines[0]).toMatch(/^http\./)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
||||||
|
'sets SSH command when persist-credentials true'
|
||||||
|
it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert git env var
|
||||||
|
const actualKeyPath = await getActualSshKeyPath()
|
||||||
|
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
||||||
|
actualKeyPath
|
||||||
|
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
||||||
|
actualKnownHostsPath
|
||||||
|
)}"`
|
||||||
|
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
|
||||||
|
'GIT_SSH_COMMAND',
|
||||||
|
expectedSshCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
// Asserty git config
|
||||||
|
expect(git.config).toHaveBeenCalledWith(
|
||||||
|
'core.sshCommand',
|
||||||
|
expectedSshCommand
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
|
||||||
|
it(configureAuth_writesExplicitKnownHosts, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(configureAuth_writesExplicitKnownHosts)
|
||||||
|
expect(settings.sshKey).toBeTruthy() // sanity check
|
||||||
|
settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123'
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert known hosts
|
||||||
|
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
const actualSshKnownHostsContent = (
|
||||||
|
await fs.promises.readFile(actualSshKnownHostsPath)
|
||||||
|
).toString()
|
||||||
|
expect(actualSshKnownHostsContent).toMatch(
|
||||||
|
/my-custom-host\.com ssh-rsa ABC123/
|
||||||
|
)
|
||||||
|
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureAuth_writesSshKeyAndImplicitKnownHosts =
|
||||||
|
'writes SSH key and implicit known hosts'
|
||||||
|
it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(configureAuth_writesSshKeyAndImplicitKnownHosts)
|
||||||
|
expect(settings.sshKey).toBeTruthy() // sanity check
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert SSH key
|
||||||
|
const actualSshKeyPath = await getActualSshKeyPath()
|
||||||
|
expect(actualSshKeyPath).toBeTruthy()
|
||||||
|
const actualSshKeyContent = (
|
||||||
|
await fs.promises.readFile(actualSshKeyPath)
|
||||||
|
).toString()
|
||||||
|
expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
|
||||||
|
if (!isWindows) {
|
||||||
|
expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
|
||||||
|
0o600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert known hosts
|
||||||
|
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
const actualSshKnownHostsContent = (
|
||||||
|
await fs.promises.readFile(actualSshKnownHostsPath)
|
||||||
|
).toString()
|
||||||
|
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet =
|
||||||
|
'configureGlobalAuth configures URL insteadOf when SSH key not set'
|
||||||
|
it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet)
|
||||||
|
settings.sshKey = ''
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureGlobalAuth()
|
||||||
|
|
||||||
|
// Assert temporary global config
|
||||||
|
expect(git.env['HOME']).toBeTruthy()
|
||||||
|
const configContent = (
|
||||||
|
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
|
).toString()
|
||||||
|
expect(
|
||||||
|
configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
const configureGlobalAuth_copiesGlobalGitConfig =
|
const configureGlobalAuth_copiesGlobalGitConfig =
|
||||||
'configureGlobalAuth copies global git config'
|
'configureGlobalAuth copies global git config'
|
||||||
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
|
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
|
||||||
|
@ -211,6 +437,67 @@ describe('git-auth-helper tests', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet =
|
||||||
|
'configureSubmoduleAuth configures token when persist credentials true and SSH key not set'
|
||||||
|
it(
|
||||||
|
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet,
|
||||||
|
async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet
|
||||||
|
)
|
||||||
|
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).toHaveBeenCalledTimes(3)
|
||||||
|
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||||
|
/unset-all.*insteadOf/
|
||||||
|
)
|
||||||
|
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
||||||
|
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/url.*insteadOf/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet =
|
||||||
|
'configureSubmoduleAuth configures token when persist credentials true and SSH key set'
|
||||||
|
it(
|
||||||
|
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet,
|
||||||
|
async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet
|
||||||
|
)
|
||||||
|
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_doesNotConfigureTokenWhenPersistCredentialsFalse =
|
const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse =
|
||||||
'configureSubmoduleAuth does not configure token when persist credentials false'
|
'configureSubmoduleAuth does not configure token when persist credentials false'
|
||||||
it(
|
it(
|
||||||
|
@ -223,37 +510,135 @@ describe('git-auth-helper tests', () => {
|
||||||
settings.persistCredentials = false
|
settings.persistCredentials = false
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||||
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(git.submoduleForeach).not.toHaveBeenCalled()
|
expect(mockSubmoduleForeach).toBeCalledTimes(1)
|
||||||
|
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
|
||||||
|
/unset-all.*insteadOf/
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue =
|
const configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet =
|
||||||
'configureSubmoduleAuth configures token when persist credentials true'
|
'configureSubmoduleAuth does not configure URL insteadOf when persist credentials true and SSH key set'
|
||||||
it(
|
it(
|
||||||
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue,
|
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet,
|
||||||
async () => {
|
async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Arrange
|
// Arrange
|
||||||
await setup(
|
await setup(
|
||||||
configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue
|
configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet
|
||||||
)
|
)
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||||
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(git.submoduleForeach).toHaveBeenCalledTimes(1)
|
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/
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeAuth_removesSshCommand = 'removeAuth removes SSH command'
|
||||||
|
it(removeAuth_removesSshCommand, async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(removeAuth_removesSshCommand)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
let gitConfigContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual(
|
||||||
|
0
|
||||||
|
) // sanity check
|
||||||
|
const actualKeyPath = await getActualSshKeyPath()
|
||||||
|
expect(actualKeyPath).toBeTruthy()
|
||||||
|
await fs.promises.stat(actualKeyPath)
|
||||||
|
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
||||||
|
expect(actualKnownHostsPath).toBeTruthy()
|
||||||
|
await fs.promises.stat(actualKnownHostsPath)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
|
// Assert git config
|
||||||
|
gitConfigContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0)
|
||||||
|
|
||||||
|
// Assert SSH key file
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(actualKeyPath)
|
||||||
|
throw new Error('SSH key should have been deleted')
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert known hosts file
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(actualKnownHostsPath)
|
||||||
|
throw new Error('SSH known hosts should have been deleted')
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const removeAuth_removesToken = 'removeAuth removes token'
|
const removeAuth_removesToken = 'removeAuth removes token'
|
||||||
it(removeAuth_removesToken, async () => {
|
it(removeAuth_removesToken, async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
@ -401,6 +786,36 @@ async function setup(testName: string): Promise<void> {
|
||||||
ref: 'refs/heads/master',
|
ref: 'refs/heads/master',
|
||||||
repositoryName: 'my-repo',
|
repositoryName: 'my-repo',
|
||||||
repositoryOwner: 'my-org',
|
repositoryOwner: 'my-org',
|
||||||
repositoryPath: ''
|
repositoryPath: '',
|
||||||
|
sshKey: sshPath ? 'some ssh private key' : '',
|
||||||
|
sshKnownHosts: '',
|
||||||
|
sshStrict: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getActualSshKeyPath(): Promise<string> {
|
||||||
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.sort()
|
||||||
|
.map(x => path.join(runnerTemp, x))
|
||||||
|
if (actualTempFiles.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(actualTempFiles).toHaveLength(2)
|
||||||
|
expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy()
|
||||||
|
return actualTempFiles[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActualSshKnownHostsPath(): Promise<string> {
|
||||||
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.sort()
|
||||||
|
.map(x => path.join(runnerTemp, x))
|
||||||
|
if (actualTempFiles.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(actualTempFiles).toHaveLength(2)
|
||||||
|
expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy()
|
||||||
|
expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy()
|
||||||
|
return actualTempFiles[1]
|
||||||
|
}
|
||||||
|
|
45
action.yml
45
action.yml
|
@ -1,6 +1,6 @@
|
||||||
name: 'Checkout'
|
name: 'Checkout'
|
||||||
description: 'Checkout a Git repository at a particular version'
|
description: 'Checkout a Git repository at a particular version'
|
||||||
inputs:
|
inputs:
|
||||||
repository:
|
repository:
|
||||||
description: 'Repository name with owner. For example, actions/checkout'
|
description: 'Repository name with owner. For example, actions/checkout'
|
||||||
default: ${{ github.repository }}
|
default: ${{ github.repository }}
|
||||||
|
@ -11,13 +11,42 @@ inputs:
|
||||||
event. Otherwise, defaults to `master`.
|
event. Otherwise, defaults to `master`.
|
||||||
token:
|
token:
|
||||||
description: >
|
description: >
|
||||||
Auth token used to fetch the repository. The token is stored in the local
|
Personal access token (PAT) used to fetch the repository. The PAT is configured
|
||||||
git config, which enables your scripts to run authenticated git commands.
|
with the local git config, which enables your scripts to run authenticated git
|
||||||
The post-job step removes the token from the git config. [Learn more about
|
commands. The post-job step removes the PAT.
|
||||||
creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
|
||||||
|
|
||||||
|
We recommend creating a service account with the least permissions necessary.
|
||||||
|
Also 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)
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
|
ssh-key:
|
||||||
|
description: >
|
||||||
|
SSH key used to fetch the repository. SSH key is configured with the local
|
||||||
|
git config, which enables your scripts to run authenticated git commands.
|
||||||
|
The post-job step removes the SSH key.
|
||||||
|
|
||||||
|
|
||||||
|
We recommend creating 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)
|
||||||
|
ssh-known-hosts:
|
||||||
|
description: >
|
||||||
|
Known hosts in addition to the user and global host key database. The public
|
||||||
|
SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example,
|
||||||
|
`ssh-keyscan github.com`. The public key for github.com is always implicitly added.
|
||||||
|
ssh-strict:
|
||||||
|
description: >
|
||||||
|
Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes`
|
||||||
|
and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to
|
||||||
|
configure additional hosts.
|
||||||
|
default: true
|
||||||
persist-credentials:
|
persist-credentials:
|
||||||
description: 'Whether to persist the token in the git config'
|
description: 'Whether to configure the token or SSH key with the local git config'
|
||||||
default: true
|
default: true
|
||||||
path:
|
path:
|
||||||
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
||||||
|
@ -34,6 +63,10 @@ inputs:
|
||||||
description: >
|
description: >
|
||||||
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
||||||
recursively checkout submodules.
|
recursively checkout submodules.
|
||||||
|
|
||||||
|
|
||||||
|
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
|
||||||
|
converted to HTTPS.
|
||||||
default: false
|
default: false
|
||||||
runs:
|
runs:
|
||||||
using: node12
|
using: node12
|
||||||
|
|
|
@ -2621,6 +2621,14 @@ exports.IsPost = !!process.env['STATE_isPost'];
|
||||||
* The repository path for the POST action. The value is empty during the MAIN action.
|
* The repository path for the POST action. The value is empty during the MAIN action.
|
||||||
*/
|
*/
|
||||||
exports.RepositoryPath = process.env['STATE_repositoryPath'] || '';
|
exports.RepositoryPath = process.env['STATE_repositoryPath'] || '';
|
||||||
|
/**
|
||||||
|
* The SSH key path for the POST action. The value is empty during the MAIN action.
|
||||||
|
*/
|
||||||
|
exports.SshKeyPath = process.env['STATE_sshKeyPath'] || '';
|
||||||
|
/**
|
||||||
|
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
|
||||||
|
*/
|
||||||
|
exports.SshKnownHostsPath = process.env['STATE_sshKnownHostsPath'] || '';
|
||||||
/**
|
/**
|
||||||
* Save the repository path so the POST action can retrieve the value.
|
* Save the repository path so the POST action can retrieve the value.
|
||||||
*/
|
*/
|
||||||
|
@ -2628,6 +2636,20 @@ function setRepositoryPath(repositoryPath) {
|
||||||
coreCommand.issueCommand('save-state', { name: 'repositoryPath' }, repositoryPath);
|
coreCommand.issueCommand('save-state', { name: 'repositoryPath' }, repositoryPath);
|
||||||
}
|
}
|
||||||
exports.setRepositoryPath = setRepositoryPath;
|
exports.setRepositoryPath = setRepositoryPath;
|
||||||
|
/**
|
||||||
|
* Save the SSH key path so the POST action can retrieve the value.
|
||||||
|
*/
|
||||||
|
function setSshKeyPath(sshKeyPath) {
|
||||||
|
coreCommand.issueCommand('save-state', { name: 'sshKeyPath' }, sshKeyPath);
|
||||||
|
}
|
||||||
|
exports.setSshKeyPath = setSshKeyPath;
|
||||||
|
/**
|
||||||
|
* Save the SSH known hosts path so the POST action can retrieve the value.
|
||||||
|
*/
|
||||||
|
function setSshKnownHostsPath(sshKnownHostsPath) {
|
||||||
|
coreCommand.issueCommand('save-state', { name: 'sshKnownHostsPath' }, sshKnownHostsPath);
|
||||||
|
}
|
||||||
|
exports.setSshKnownHostsPath = setSshKnownHostsPath;
|
||||||
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
|
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
|
||||||
// This is necessary since we don't have a separate entry point.
|
// This is necessary since we don't have a separate entry point.
|
||||||
if (!exports.IsPost) {
|
if (!exports.IsPost) {
|
||||||
|
@ -5080,14 +5102,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const assert = __importStar(__webpack_require__(357));
|
const assert = __importStar(__webpack_require__(357));
|
||||||
const core = __importStar(__webpack_require__(470));
|
const core = __importStar(__webpack_require__(470));
|
||||||
|
const exec = __importStar(__webpack_require__(986));
|
||||||
const fs = __importStar(__webpack_require__(747));
|
const fs = __importStar(__webpack_require__(747));
|
||||||
const io = __importStar(__webpack_require__(1));
|
const io = __importStar(__webpack_require__(1));
|
||||||
const os = __importStar(__webpack_require__(87));
|
const os = __importStar(__webpack_require__(87));
|
||||||
const path = __importStar(__webpack_require__(622));
|
const path = __importStar(__webpack_require__(622));
|
||||||
const regexpHelper = __importStar(__webpack_require__(528));
|
const regexpHelper = __importStar(__webpack_require__(528));
|
||||||
|
const stateHelper = __importStar(__webpack_require__(153));
|
||||||
const v4_1 = __importDefault(__webpack_require__(826));
|
const v4_1 = __importDefault(__webpack_require__(826));
|
||||||
const IS_WINDOWS = process.platform === 'win32';
|
const IS_WINDOWS = process.platform === 'win32';
|
||||||
const HOSTNAME = 'github.com';
|
const HOSTNAME = 'github.com';
|
||||||
|
const SSH_COMMAND_KEY = 'core.sshCommand';
|
||||||
function createAuthHelper(git, settings) {
|
function createAuthHelper(git, settings) {
|
||||||
return new GitAuthHelper(git, settings);
|
return new GitAuthHelper(git, settings);
|
||||||
}
|
}
|
||||||
|
@ -5097,6 +5122,8 @@ 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.sshKeyPath = '';
|
||||||
|
this.sshKnownHostsPath = '';
|
||||||
this.temporaryHomePath = '';
|
this.temporaryHomePath = '';
|
||||||
this.git = gitCommandManager;
|
this.git = gitCommandManager;
|
||||||
this.settings = gitSourceSettings || {};
|
this.settings = gitSourceSettings || {};
|
||||||
|
@ -5111,6 +5138,7 @@ class GitAuthHelper {
|
||||||
// Remove possible previous values
|
// Remove possible previous values
|
||||||
yield this.removeAuth();
|
yield this.removeAuth();
|
||||||
// Configure new values
|
// Configure new values
|
||||||
|
yield this.configureSsh();
|
||||||
yield this.configureToken();
|
yield this.configureToken();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5150,7 +5178,9 @@ class GitAuthHelper {
|
||||||
yield this.configureToken(newGitConfigPath, true);
|
yield this.configureToken(newGitConfigPath, true);
|
||||||
// Configure HTTPS instead of SSH
|
// Configure HTTPS instead of SSH
|
||||||
yield this.git.tryConfigUnset(this.insteadOfKey, true);
|
yield this.git.tryConfigUnset(this.insteadOfKey, true);
|
||||||
yield this.git.config(this.insteadOfKey, this.insteadOfValue, true);
|
if (!this.settings.sshKey) {
|
||||||
|
yield this.git.config(this.insteadOfKey, this.insteadOfValue, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// Unset in case somehow written to the real global config
|
// Unset in case somehow written to the real global config
|
||||||
|
@ -5162,27 +5192,29 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
configureSubmoduleAuth() {
|
configureSubmoduleAuth() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Remove possible previous HTTPS instead of SSH
|
||||||
|
yield this.removeGitConfig(this.insteadOfKey, true);
|
||||||
if (this.settings.persistCredentials) {
|
if (this.settings.persistCredentials) {
|
||||||
// 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
|
||||||
const commands = [
|
const output = yield this.git.submoduleForeach(`git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules);
|
||||||
`git config --local "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}"`,
|
|
||||||
`git config --local "${this.insteadOfKey}" "${this.insteadOfValue}"`,
|
|
||||||
`git config --local --show-origin --name-only --get-regexp remote.origin.url`
|
|
||||||
];
|
|
||||||
const output = yield this.git.submoduleForeach(commands.join(' && '), this.settings.nestedSubmodules);
|
|
||||||
// Replace the placeholder
|
// Replace the placeholder
|
||||||
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
|
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
|
||||||
for (const configPath of configPaths) {
|
for (const configPath of configPaths) {
|
||||||
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) {
|
||||||
|
yield this.git.submoduleForeach(`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, this.settings.nestedSubmodules);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
removeAuth() {
|
removeAuth() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
yield this.removeSsh();
|
||||||
yield this.removeToken();
|
yield this.removeToken();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5193,6 +5225,62 @@ class GitAuthHelper {
|
||||||
yield io.rmRF(this.temporaryHomePath);
|
yield io.rmRF(this.temporaryHomePath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
configureSsh() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if (!this.settings.sshKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Write key
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || '';
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
|
||||||
|
const uniqueId = v4_1.default();
|
||||||
|
this.sshKeyPath = path.join(runnerTemp, uniqueId);
|
||||||
|
stateHelper.setSshKeyPath(this.sshKeyPath);
|
||||||
|
yield fs.promises.mkdir(runnerTemp, { recursive: true });
|
||||||
|
yield fs.promises.writeFile(this.sshKeyPath, this.settings.sshKey.trim() + '\n', { mode: 0o600 });
|
||||||
|
// Remove inherited permissions on Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const icacls = yield io.which('icacls.exe');
|
||||||
|
yield exec.exec(`"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`);
|
||||||
|
yield exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`);
|
||||||
|
}
|
||||||
|
// Write known hosts
|
||||||
|
const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts');
|
||||||
|
let userKnownHosts = '';
|
||||||
|
try {
|
||||||
|
userKnownHosts = (yield fs.promises.readFile(userKnownHostsPath)).toString();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let knownHosts = '';
|
||||||
|
if (userKnownHosts) {
|
||||||
|
knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`;
|
||||||
|
}
|
||||||
|
if (this.settings.sshKnownHosts) {
|
||||||
|
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`;
|
||||||
|
}
|
||||||
|
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`;
|
||||||
|
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`);
|
||||||
|
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath);
|
||||||
|
yield fs.promises.writeFile(this.sshKnownHostsPath, knownHosts);
|
||||||
|
// Configure GIT_SSH_COMMAND
|
||||||
|
const sshPath = yield io.which('ssh', true);
|
||||||
|
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`;
|
||||||
|
if (this.settings.sshStrict) {
|
||||||
|
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no';
|
||||||
|
}
|
||||||
|
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`;
|
||||||
|
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`);
|
||||||
|
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand);
|
||||||
|
// Configure core.sshCommand
|
||||||
|
if (this.settings.persistCredentials) {
|
||||||
|
yield this.git.config(SSH_COMMAND_KEY, sshCommand);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
configureToken(configPath, globalConfig) {
|
configureToken(configPath, globalConfig) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Validate args
|
// Validate args
|
||||||
|
@ -5223,21 +5311,50 @@ class GitAuthHelper {
|
||||||
yield fs.promises.writeFile(configPath, content);
|
yield fs.promises.writeFile(configPath, content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
removeSsh() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// SSH key
|
||||||
|
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
|
||||||
|
if (keyPath) {
|
||||||
|
try {
|
||||||
|
yield io.rmRF(keyPath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
core.debug(err.message);
|
||||||
|
core.warning(`Failed to remove SSH key '${keyPath}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SSH known hosts
|
||||||
|
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
|
||||||
|
if (knownHostsPath) {
|
||||||
|
try {
|
||||||
|
yield io.rmRF(knownHostsPath);
|
||||||
|
}
|
||||||
|
catch (_a) {
|
||||||
|
// Intentionally empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SSH command
|
||||||
|
yield this.removeGitConfig(SSH_COMMAND_KEY);
|
||||||
|
});
|
||||||
|
}
|
||||||
removeToken() {
|
removeToken() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// HTTP extra header
|
// HTTP extra header
|
||||||
yield this.removeGitConfig(this.tokenConfigKey);
|
yield this.removeGitConfig(this.tokenConfigKey);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
removeGitConfig(configKey) {
|
removeGitConfig(configKey, submoduleOnly = false) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
if ((yield this.git.configExists(configKey)) &&
|
if (!submoduleOnly) {
|
||||||
!(yield this.git.tryConfigUnset(configKey))) {
|
if ((yield this.git.configExists(configKey)) &&
|
||||||
// Load the config contents
|
!(yield this.git.tryConfigUnset(configKey))) {
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`);
|
// Load the config contents
|
||||||
|
core.warning(`Failed to remove '${configKey}' from the git config`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const pattern = regexpHelper.escape(configKey);
|
const pattern = regexpHelper.escape(configKey);
|
||||||
yield this.git.submoduleForeach(`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`, true);
|
yield this.git.submoduleForeach(`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5680,7 +5797,9 @@ function getSource(settings) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Repository URL
|
// Repository URL
|
||||||
core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`);
|
core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`);
|
||||||
const repositoryUrl = `https://${hostname}/${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}`;
|
const repositoryUrl = settings.sshKey
|
||||||
|
? `git@${hostname}:${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}.git`
|
||||||
|
: `https://${hostname}/${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}`;
|
||||||
// Remove conflicting file path
|
// Remove conflicting file path
|
||||||
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
||||||
yield io.rmRF(settings.repositoryPath);
|
yield io.rmRF(settings.repositoryPath);
|
||||||
|
@ -13940,6 +14059,11 @@ function getInputs() {
|
||||||
core.debug(`recursive submodules = ${result.nestedSubmodules}`);
|
core.debug(`recursive submodules = ${result.nestedSubmodules}`);
|
||||||
// Auth token
|
// Auth token
|
||||||
result.authToken = core.getInput('token');
|
result.authToken = core.getInput('token');
|
||||||
|
// SSH
|
||||||
|
result.sshKey = core.getInput('ssh-key');
|
||||||
|
result.sshKnownHosts = core.getInput('ssh-known-hosts');
|
||||||
|
result.sshStrict =
|
||||||
|
(core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE';
|
||||||
// Persist credentials
|
// Persist credentials
|
||||||
result.persistCredentials =
|
result.persistCredentials =
|
||||||
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE';
|
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE';
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
const HOSTNAME = 'github.com'
|
const HOSTNAME = 'github.com'
|
||||||
|
const SSH_COMMAND_KEY = 'core.sshCommand'
|
||||||
|
|
||||||
export interface IGitAuthHelper {
|
export interface IGitAuthHelper {
|
||||||
configureAuth(): Promise<void>
|
configureAuth(): Promise<void>
|
||||||
|
@ -36,6 +37,8 @@ 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 sshKeyPath = ''
|
||||||
|
private sshKnownHostsPath = ''
|
||||||
private temporaryHomePath = ''
|
private temporaryHomePath = ''
|
||||||
private tokenConfigValue: string
|
private tokenConfigValue: string
|
||||||
|
|
||||||
|
@ -61,6 +64,7 @@ class GitAuthHelper {
|
||||||
await this.removeAuth()
|
await this.removeAuth()
|
||||||
|
|
||||||
// Configure new values
|
// Configure new values
|
||||||
|
await this.configureSsh()
|
||||||
await this.configureToken()
|
await this.configureToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +110,9 @@ class GitAuthHelper {
|
||||||
|
|
||||||
// Configure HTTPS instead of SSH
|
// Configure HTTPS instead of SSH
|
||||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||||
await this.git.config(this.insteadOfKey, this.insteadOfValue, true)
|
if (!this.settings.sshKey) {
|
||||||
|
await this.git.config(this.insteadOfKey, this.insteadOfValue, true)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Unset in case somehow written to the real global config
|
// Unset in case somehow written to the real global config
|
||||||
core.info(
|
core.info(
|
||||||
|
@ -118,17 +124,15 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
async configureSubmoduleAuth(): Promise<void> {
|
async configureSubmoduleAuth(): Promise<void> {
|
||||||
|
// Remove possible previous HTTPS instead of SSH
|
||||||
|
await this.removeGitConfig(this.insteadOfKey, true)
|
||||||
|
|
||||||
if (this.settings.persistCredentials) {
|
if (this.settings.persistCredentials) {
|
||||||
// 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
|
||||||
const commands = [
|
|
||||||
`git config --local "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}"`,
|
|
||||||
`git config --local "${this.insteadOfKey}" "${this.insteadOfValue}"`,
|
|
||||||
`git config --local --show-origin --name-only --get-regexp remote.origin.url`
|
|
||||||
]
|
|
||||||
const output = await this.git.submoduleForeach(
|
const output = await this.git.submoduleForeach(
|
||||||
commands.join(' && '),
|
`git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`,
|
||||||
this.settings.nestedSubmodules
|
this.settings.nestedSubmodules
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -139,10 +143,19 @@ 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) {
|
||||||
|
await this.git.submoduleForeach(
|
||||||
|
`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`,
|
||||||
|
this.settings.nestedSubmodules
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAuth(): Promise<void> {
|
async removeAuth(): Promise<void> {
|
||||||
|
await this.removeSsh()
|
||||||
await this.removeToken()
|
await this.removeToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +165,77 @@ class GitAuthHelper {
|
||||||
await io.rmRF(this.temporaryHomePath)
|
await io.rmRF(this.temporaryHomePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async configureSsh(): Promise<void> {
|
||||||
|
if (!this.settings.sshKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write key
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
|
const uniqueId = uuid()
|
||||||
|
this.sshKeyPath = path.join(runnerTemp, uniqueId)
|
||||||
|
stateHelper.setSshKeyPath(this.sshKeyPath)
|
||||||
|
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
this.sshKeyPath,
|
||||||
|
this.settings.sshKey.trim() + '\n',
|
||||||
|
{mode: 0o600}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove inherited permissions on Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const icacls = await io.which('icacls.exe')
|
||||||
|
await exec.exec(
|
||||||
|
`"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`
|
||||||
|
)
|
||||||
|
await exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write known hosts
|
||||||
|
const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts')
|
||||||
|
let userKnownHosts = ''
|
||||||
|
try {
|
||||||
|
userKnownHosts = (
|
||||||
|
await fs.promises.readFile(userKnownHostsPath)
|
||||||
|
).toString()
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let knownHosts = ''
|
||||||
|
if (userKnownHosts) {
|
||||||
|
knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`
|
||||||
|
}
|
||||||
|
if (this.settings.sshKnownHosts) {
|
||||||
|
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
|
||||||
|
}
|
||||||
|
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`
|
||||||
|
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
|
||||||
|
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
|
||||||
|
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
|
||||||
|
|
||||||
|
// Configure GIT_SSH_COMMAND
|
||||||
|
const sshPath = await io.which('ssh', true)
|
||||||
|
let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
||||||
|
this.sshKeyPath
|
||||||
|
)}"`
|
||||||
|
if (this.settings.sshStrict) {
|
||||||
|
sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
|
||||||
|
}
|
||||||
|
sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
||||||
|
this.sshKnownHostsPath
|
||||||
|
)}"`
|
||||||
|
core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`)
|
||||||
|
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand)
|
||||||
|
|
||||||
|
// Configure core.sshCommand
|
||||||
|
if (this.settings.persistCredentials) {
|
||||||
|
await this.git.config(SSH_COMMAND_KEY, sshCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async configureToken(
|
private async configureToken(
|
||||||
configPath?: string,
|
configPath?: string,
|
||||||
globalConfig?: boolean
|
globalConfig?: boolean
|
||||||
|
@ -198,23 +282,55 @@ class GitAuthHelper {
|
||||||
await fs.promises.writeFile(configPath, content)
|
await fs.promises.writeFile(configPath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async removeSsh(): Promise<void> {
|
||||||
|
// SSH key
|
||||||
|
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||||
|
if (keyPath) {
|
||||||
|
try {
|
||||||
|
await io.rmRF(keyPath)
|
||||||
|
} catch (err) {
|
||||||
|
core.debug(err.message)
|
||||||
|
core.warning(`Failed to remove SSH key '${keyPath}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH known hosts
|
||||||
|
const knownHostsPath =
|
||||||
|
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||||
|
if (knownHostsPath) {
|
||||||
|
try {
|
||||||
|
await io.rmRF(knownHostsPath)
|
||||||
|
} catch {
|
||||||
|
// Intentionally empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH command
|
||||||
|
await this.removeGitConfig(SSH_COMMAND_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
private async removeToken(): Promise<void> {
|
private async removeToken(): Promise<void> {
|
||||||
// HTTP extra header
|
// HTTP extra header
|
||||||
await this.removeGitConfig(this.tokenConfigKey)
|
await this.removeGitConfig(this.tokenConfigKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeGitConfig(configKey: string): Promise<void> {
|
private async removeGitConfig(
|
||||||
if (
|
configKey: string,
|
||||||
(await this.git.configExists(configKey)) &&
|
submoduleOnly: boolean = false
|
||||||
!(await this.git.tryConfigUnset(configKey))
|
): Promise<void> {
|
||||||
) {
|
if (!submoduleOnly) {
|
||||||
// Load the config contents
|
if (
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
(await this.git.configExists(configKey)) &&
|
||||||
|
!(await this.git.tryConfigUnset(configKey))
|
||||||
|
) {
|
||||||
|
// Load the config contents
|
||||||
|
core.warning(`Failed to remove '${configKey}' from the git config`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pattern = regexpHelper.escape(configKey)
|
const pattern = regexpHelper.escape(configKey)
|
||||||
await this.git.submoduleForeach(
|
await this.git.submoduleForeach(
|
||||||
`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`,
|
`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,13 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
core.info(
|
core.info(
|
||||||
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
|
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
|
||||||
)
|
)
|
||||||
const repositoryUrl = `https://${hostname}/${encodeURIComponent(
|
const repositoryUrl = settings.sshKey
|
||||||
settings.repositoryOwner
|
? `git@${hostname}:${encodeURIComponent(
|
||||||
)}/${encodeURIComponent(settings.repositoryName)}`
|
settings.repositoryOwner
|
||||||
|
)}/${encodeURIComponent(settings.repositoryName)}.git`
|
||||||
|
: `https://${hostname}/${encodeURIComponent(
|
||||||
|
settings.repositoryOwner
|
||||||
|
)}/${encodeURIComponent(settings.repositoryName)}`
|
||||||
|
|
||||||
// Remove conflicting file path
|
// Remove conflicting file path
|
||||||
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
||||||
|
|
|
@ -10,5 +10,8 @@ export interface IGitSourceSettings {
|
||||||
submodules: boolean
|
submodules: boolean
|
||||||
nestedSubmodules: boolean
|
nestedSubmodules: boolean
|
||||||
authToken: string
|
authToken: string
|
||||||
|
sshKey: string
|
||||||
|
sshKnownHosts: string
|
||||||
|
sshStrict: boolean
|
||||||
persistCredentials: boolean
|
persistCredentials: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,12 @@ export function getInputs(): IGitSourceSettings {
|
||||||
// Auth token
|
// Auth token
|
||||||
result.authToken = core.getInput('token')
|
result.authToken = core.getInput('token')
|
||||||
|
|
||||||
|
// SSH
|
||||||
|
result.sshKey = core.getInput('ssh-key')
|
||||||
|
result.sshKnownHosts = core.getInput('ssh-known-hosts')
|
||||||
|
result.sshStrict =
|
||||||
|
(core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
|
||||||
|
|
||||||
// Persist credentials
|
// Persist credentials
|
||||||
result.persistCredentials =
|
result.persistCredentials =
|
||||||
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
|
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
|
||||||
|
|
|
@ -59,13 +59,17 @@ function updateUsage(
|
||||||
|
|
||||||
// Constrain the width of the description
|
// Constrain the width of the description
|
||||||
const width = 80
|
const width = 80
|
||||||
let description = input.description as string
|
let description = (input.description as string)
|
||||||
|
.trimRight()
|
||||||
|
.replace(/\r\n/g, '\n') // Convert CR to LF
|
||||||
|
.replace(/ +/g, ' ') // Squash consecutive spaces
|
||||||
|
.replace(/ \n/g, '\n') // Squash space followed by newline
|
||||||
while (description) {
|
while (description) {
|
||||||
// Longer than width? Find a space to break apart
|
// Longer than width? Find a space to break apart
|
||||||
let segment: string = description
|
let segment: string = description
|
||||||
if (description.length > width) {
|
if (description.length > width) {
|
||||||
segment = description.substr(0, width + 1)
|
segment = description.substr(0, width + 1)
|
||||||
while (!segment.endsWith(' ') && segment) {
|
while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) {
|
||||||
segment = segment.substr(0, segment.length - 1)
|
segment = segment.substr(0, segment.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,15 +81,30 @@ function updateUsage(
|
||||||
segment = description
|
segment = description
|
||||||
}
|
}
|
||||||
|
|
||||||
description = description.substr(segment.length) // Remaining
|
// Check for newline
|
||||||
segment = segment.trimRight() // Trim the trailing space
|
const newlineIndex = segment.indexOf('\n')
|
||||||
newReadme.push(` # ${segment}`)
|
if (newlineIndex >= 0) {
|
||||||
|
segment = segment.substr(0, newlineIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append segment
|
||||||
|
newReadme.push(` # ${segment}`.trimRight())
|
||||||
|
|
||||||
|
// Remaining
|
||||||
|
description = description.substr(segment.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input and default
|
|
||||||
if (input.default !== undefined) {
|
if (input.default !== undefined) {
|
||||||
|
// Append blank line if description had paragraphs
|
||||||
|
if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) {
|
||||||
|
newReadme.push(` #`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
newReadme.push(` # Default: ${input.default}`)
|
newReadme.push(` # Default: ${input.default}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input name
|
||||||
newReadme.push(` ${key}: ''`)
|
newReadme.push(` ${key}: ''`)
|
||||||
|
|
||||||
firstInput = false
|
firstInput = false
|
||||||
|
|
|
@ -11,6 +11,17 @@ export const IsPost = !!process.env['STATE_isPost']
|
||||||
export const RepositoryPath =
|
export const RepositoryPath =
|
||||||
(process.env['STATE_repositoryPath'] as string) || ''
|
(process.env['STATE_repositoryPath'] as string) || ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SSH key path for the POST action. The value is empty during the MAIN action.
|
||||||
|
*/
|
||||||
|
export const SshKeyPath = (process.env['STATE_sshKeyPath'] as string) || ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
|
||||||
|
*/
|
||||||
|
export const SshKnownHostsPath =
|
||||||
|
(process.env['STATE_sshKnownHostsPath'] as string) || ''
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the repository path so the POST action can retrieve the value.
|
* Save the repository path so the POST action can retrieve the value.
|
||||||
*/
|
*/
|
||||||
|
@ -22,6 +33,24 @@ export function setRepositoryPath(repositoryPath: string) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the SSH key path so the POST action can retrieve the value.
|
||||||
|
*/
|
||||||
|
export function setSshKeyPath(sshKeyPath: string) {
|
||||||
|
coreCommand.issueCommand('save-state', {name: 'sshKeyPath'}, sshKeyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the SSH known hosts path so the POST action can retrieve the value.
|
||||||
|
*/
|
||||||
|
export function setSshKnownHostsPath(sshKnownHostsPath: string) {
|
||||||
|
coreCommand.issueCommand(
|
||||||
|
'save-state',
|
||||||
|
{name: 'sshKnownHostsPath'},
|
||||||
|
sshKnownHostsPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
|
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
|
||||||
// This is necessary since we don't have a separate entry point.
|
// This is necessary since we don't have a separate entry point.
|
||||||
if (!IsPost) {
|
if (!IsPost) {
|
||||||
|
|
Loading…
Reference in New Issue