Merge branch 'master' into patch-1
commit
beb483bbd0
@ -1,2 +1,3 @@
|
|||||||
|
__test__/_temp
|
||||||
lib/
|
lib/
|
||||||
node_modules/
|
node_modules/
|
@ -0,0 +1,802 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as gitAuthHelper from '../lib/git-auth-helper'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as stateHelper from '../lib/state-helper'
|
||||||
|
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||||
|
import {IGitSourceSettings} from '../lib/git-source-settings'
|
||||||
|
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
|
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
||||||
|
const originalRunnerTemp = process.env['RUNNER_TEMP']
|
||||||
|
const originalHome = process.env['HOME']
|
||||||
|
let workspace: string
|
||||||
|
let localGitConfigPath: string
|
||||||
|
let globalGitConfigPath: string
|
||||||
|
let runnerTemp: string
|
||||||
|
let tempHomedir: string
|
||||||
|
let git: IGitCommandManager & {env: {[key: string]: string}}
|
||||||
|
let settings: IGitSourceSettings
|
||||||
|
let sshPath: string
|
||||||
|
|
||||||
|
describe('git-auth-helper tests', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// SSH
|
||||||
|
sshPath = await io.which('ssh')
|
||||||
|
|
||||||
|
// Clear test workspace
|
||||||
|
await io.rmRF(testWorkspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock setSecret
|
||||||
|
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
|
||||||
|
|
||||||
|
// Mock error/warning/info/debug
|
||||||
|
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(core, 'info').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(() => {
|
||||||
|
// Unregister mocks
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
|
||||||
|
// Restore HOME
|
||||||
|
if (originalHome) {
|
||||||
|
process.env['HOME'] = originalHome
|
||||||
|
} else {
|
||||||
|
delete process.env['HOME']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Restore RUNNER_TEMP
|
||||||
|
delete process.env['RUNNER_TEMP']
|
||||||
|
if (originalRunnerTemp) {
|
||||||
|
process.env['RUNNER_TEMP'] = originalRunnerTemp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureAuth_configuresAuthHeader =
|
||||||
|
'configureAuth configures auth header'
|
||||||
|
it(configureAuth_configuresAuthHeader, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(configureAuth_configuresAuthHeader)
|
||||||
|
expect(settings.authToken).toBeTruthy() // sanity check
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert config
|
||||||
|
const configContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
const basicCredential = Buffer.from(
|
||||||
|
`x-access-token:${settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
expect(
|
||||||
|
configContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
|
||||||
|
'configureAuth configures auth header even when persist credentials false'
|
||||||
|
it(
|
||||||
|
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
|
||||||
|
async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
|
||||||
|
)
|
||||||
|
expect(settings.authToken).toBeTruthy() // sanity check
|
||||||
|
settings.persistCredentials = false
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert config
|
||||||
|
const configContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(
|
||||||
|
configContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION`
|
||||||
|
)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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 =
|
||||||
|
'configureAuth registers basic credential as secret'
|
||||||
|
it(configureAuth_registersBasicCredentialAsSecret, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(configureAuth_registersBasicCredentialAsSecret)
|
||||||
|
expect(settings.authToken).toBeTruthy() // sanity check
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
// Assert secret
|
||||||
|
const setSecretSpy = core.setSecret as jest.Mock<any, any>
|
||||||
|
expect(setSecretSpy).toHaveBeenCalledTimes(1)
|
||||||
|
const expectedSecret = Buffer.from(
|
||||||
|
`x-access-token:${settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
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) {
|
||||||
|
// Assert read/write for user, not group or others.
|
||||||
|
// Otherwise SSH client will error.
|
||||||
|
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 =
|
||||||
|
'configureGlobalAuth copies global git config'
|
||||||
|
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(configureGlobalAuth_copiesGlobalGitConfig)
|
||||||
|
await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureGlobalAuth()
|
||||||
|
|
||||||
|
// Assert original global config not altered
|
||||||
|
let configContent = (
|
||||||
|
await fs.promises.readFile(globalGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(configContent).toBe('value-from-global-config')
|
||||||
|
|
||||||
|
// Assert temporary global config
|
||||||
|
expect(git.env['HOME']).toBeTruthy()
|
||||||
|
const basicCredential = Buffer.from(
|
||||||
|
`x-access-token:${settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
configContent = (
|
||||||
|
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
|
).toString()
|
||||||
|
expect(
|
||||||
|
configContent.indexOf('value-from-global-config')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
configContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
|
||||||
|
'configureGlobalAuth creates new git config when global does not exist'
|
||||||
|
it(
|
||||||
|
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
|
||||||
|
async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
|
||||||
|
)
|
||||||
|
await io.rmRF(globalGitConfigPath)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureGlobalAuth()
|
||||||
|
|
||||||
|
// Assert original global config not recreated
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(globalGitConfigPath)
|
||||||
|
throw new Error(
|
||||||
|
`Did not expect file to exist: '${globalGitConfigPath}'`
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert temporary global config
|
||||||
|
expect(git.env['HOME']).toBeTruthy()
|
||||||
|
const basicCredential = Buffer.from(
|
||||||
|
`x-access-token:${settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
const configContent = (
|
||||||
|
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
|
).toString()
|
||||||
|
expect(
|
||||||
|
configContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
|
||||||
|
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
|
||||||
|
it(
|
||||||
|
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
|
||||||
|
async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
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 = ''
|
||||||
|
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_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
|
||||||
|
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
|
||||||
|
it(
|
||||||
|
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
|
||||||
|
async () => {
|
||||||
|
if (!sshPath) {
|
||||||
|
process.stdout.write(
|
||||||
|
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
await setup(
|
||||||
|
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
|
||||||
|
)
|
||||||
|
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(/core\.sshCommand/)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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'
|
||||||
|
it(removeAuth_removesToken, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removeAuth_removesToken)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
let gitConfigContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
|
// Assert git config
|
||||||
|
gitConfigContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
|
||||||
|
it(removeGlobalAuth_removesOverride, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removeGlobalAuth_removesOverride)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureGlobalAuth()
|
||||||
|
const homeOverride = git.env['HOME'] // Sanity check
|
||||||
|
expect(homeOverride).toBeTruthy()
|
||||||
|
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await authHelper.removeGlobalAuth()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(git.env['HOME']).toBeUndefined()
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(homeOverride)
|
||||||
|
throw new Error(`Should have been deleted '${homeOverride}'`)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function setup(testName: string): Promise<void> {
|
||||||
|
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
|
||||||
|
|
||||||
|
// Directories
|
||||||
|
workspace = path.join(testWorkspace, testName, 'workspace')
|
||||||
|
runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
|
||||||
|
tempHomedir = path.join(testWorkspace, testName, 'home-dir')
|
||||||
|
await fs.promises.mkdir(workspace, {recursive: true})
|
||||||
|
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
||||||
|
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
||||||
|
process.env['RUNNER_TEMP'] = runnerTemp
|
||||||
|
process.env['HOME'] = tempHomedir
|
||||||
|
|
||||||
|
// Create git config
|
||||||
|
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
||||||
|
await fs.promises.writeFile(globalGitConfigPath, '')
|
||||||
|
localGitConfigPath = path.join(workspace, '.git', 'config')
|
||||||
|
await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
|
||||||
|
await fs.promises.writeFile(localGitConfigPath, '')
|
||||||
|
|
||||||
|
git = {
|
||||||
|
branchDelete: jest.fn(),
|
||||||
|
branchExists: jest.fn(),
|
||||||
|
branchList: jest.fn(),
|
||||||
|
checkout: jest.fn(),
|
||||||
|
checkoutDetach: jest.fn(),
|
||||||
|
config: jest.fn(
|
||||||
|
async (key: string, value: string, globalConfig?: boolean) => {
|
||||||
|
const configPath = globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath
|
||||||
|
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
configExists: jest.fn(
|
||||||
|
async (key: string, globalConfig?: boolean): Promise<boolean> => {
|
||||||
|
const configPath = globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath
|
||||||
|
const content = await fs.promises.readFile(configPath)
|
||||||
|
const lines = content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x)
|
||||||
|
return lines.some(x => x.startsWith(key))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
env: {},
|
||||||
|
fetch: jest.fn(),
|
||||||
|
getDefaultBranch: jest.fn(),
|
||||||
|
getWorkingDirectory: jest.fn(() => workspace),
|
||||||
|
init: jest.fn(),
|
||||||
|
isDetached: jest.fn(),
|
||||||
|
lfsFetch: jest.fn(),
|
||||||
|
lfsInstall: jest.fn(),
|
||||||
|
log1: jest.fn(),
|
||||||
|
remoteAdd: jest.fn(),
|
||||||
|
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
|
||||||
|
revParse: jest.fn(),
|
||||||
|
setEnvironmentVariable: jest.fn((name: string, value: string) => {
|
||||||
|
git.env[name] = value
|
||||||
|
}),
|
||||||
|
shaExists: jest.fn(),
|
||||||
|
submoduleForeach: jest.fn(async () => {
|
||||||
|
return ''
|
||||||
|
}),
|
||||||
|
submoduleSync: jest.fn(),
|
||||||
|
submoduleUpdate: jest.fn(),
|
||||||
|
tagExists: jest.fn(),
|
||||||
|
tryClean: jest.fn(),
|
||||||
|
tryConfigUnset: jest.fn(
|
||||||
|
async (key: string, globalConfig?: boolean): Promise<boolean> => {
|
||||||
|
const configPath = globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath
|
||||||
|
let content = await fs.promises.readFile(configPath)
|
||||||
|
let lines = content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x)
|
||||||
|
.filter(x => !x.startsWith(key))
|
||||||
|
await fs.promises.writeFile(configPath, lines.join('\n'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
|
tryGetFetchUrl: jest.fn(),
|
||||||
|
tryReset: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
authToken: 'some auth token',
|
||||||
|
clean: true,
|
||||||
|
commit: '',
|
||||||
|
fetchDepth: 1,
|
||||||
|
lfs: false,
|
||||||
|
submodules: false,
|
||||||
|
nestedSubmodules: false,
|
||||||
|
persistCredentials: true,
|
||||||
|
ref: 'refs/heads/master',
|
||||||
|
repositoryName: 'my-repo',
|
||||||
|
repositoryOwner: 'my-org',
|
||||||
|
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]
|
||||||
|
}
|
@ -0,0 +1,441 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as gitDirectoryHelper from '../lib/git-directory-helper'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as path from 'path'
|
||||||
|
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||||
|
|
||||||
|
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
||||||
|
let repositoryPath: string
|
||||||
|
let repositoryUrl: string
|
||||||
|
let clean: boolean
|
||||||
|
let ref: string
|
||||||
|
let git: IGitCommandManager
|
||||||
|
|
||||||
|
describe('git-directory-helper tests', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Clear test workspace
|
||||||
|
await io.rmRF(testWorkspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock error/warning/info/debug
|
||||||
|
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Unregister mocks
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const cleansWhenCleanTrue = 'cleans when clean true'
|
||||||
|
it(cleansWhenCleanTrue, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(cleansWhenCleanTrue)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).toHaveBeenCalled()
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
|
||||||
|
it(checkoutDetachWhenNotDetached, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(checkoutDetachWhenNotDetached)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.checkoutDetach).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const doesNotCheckoutDetachWhenNotAlreadyDetached =
|
||||||
|
'does not checkout detach when already detached'
|
||||||
|
it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
const mockIsDetached = git.isDetached as jest.Mock<any, any>
|
||||||
|
mockIsDetached.mockImplementation(async () => {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.checkoutDetach).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
|
||||||
|
it(doesNotCleanWhenCleanFalse, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(doesNotCleanWhenCleanFalse)
|
||||||
|
clean = false
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.isDetached).toHaveBeenCalled()
|
||||||
|
expect(git.branchList).toHaveBeenCalled()
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(git.tryClean).not.toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesContentsWhenCleanFails = 'removes contents when clean fails'
|
||||||
|
it(removesContentsWhenCleanFails, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesContentsWhenCleanFails)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
||||||
|
mockTryClean.mockImplementation(async () => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
|
expect(core.warning).toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesContentsWhenDifferentRepositoryUrl =
|
||||||
|
'removes contents when different repository url'
|
||||||
|
it(removesContentsWhenDifferentRepositoryUrl, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesContentsWhenDifferentRepositoryUrl)
|
||||||
|
clean = false
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
const differentRepositoryUrl =
|
||||||
|
'https://github.com/my-different-org/my-different-repo'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
differentRepositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(git.isDetached).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesContentsWhenNoGitDirectory =
|
||||||
|
'removes contents when no git directory'
|
||||||
|
it(removesContentsWhenNoGitDirectory, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesContentsWhenNoGitDirectory)
|
||||||
|
clean = false
|
||||||
|
await io.rmRF(path.join(repositoryPath, '.git'))
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(git.isDetached).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesContentsWhenResetFails = 'removes contents when reset fails'
|
||||||
|
it(removesContentsWhenResetFails, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesContentsWhenResetFails)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
||||||
|
mockTryReset.mockImplementation(async () => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).toHaveBeenCalled()
|
||||||
|
expect(core.warning).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesContentsWhenUndefinedGitCommandManager =
|
||||||
|
'removes contents when undefined git command manager'
|
||||||
|
it(removesContentsWhenUndefinedGitCommandManager, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesContentsWhenUndefinedGitCommandManager)
|
||||||
|
clean = false
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
undefined,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesLocalBranches = 'removes local branches'
|
||||||
|
it(removesLocalBranches, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesLocalBranches)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||||
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
|
return remote ? [] : ['local-branch-1', 'local-branch-2']
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1')
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesLockFiles = 'removes lock files'
|
||||||
|
it(removesLockFiles, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesLockFiles)
|
||||||
|
clean = false
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(repositoryPath, '.git', 'index.lock'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(repositoryPath, '.git', 'shallow.lock'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
|
||||||
|
expect(files).toHaveLength(0)
|
||||||
|
files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.isDetached).toHaveBeenCalled()
|
||||||
|
expect(git.branchList).toHaveBeenCalled()
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(git.tryClean).not.toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesAncestorRemoteBranch = 'removes ancestor remote branch'
|
||||||
|
it(removesAncestorRemoteBranch, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesAncestorRemoteBranch)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||||
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
|
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
|
||||||
|
})
|
||||||
|
ref = 'remote-branch-1/conflict'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledTimes(1)
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledWith(
|
||||||
|
true,
|
||||||
|
'origin/remote-branch-1'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const removesDescendantRemoteBranches = 'removes descendant remote branch'
|
||||||
|
it(removesDescendantRemoteBranches, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removesDescendantRemoteBranches)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||||
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
|
return remote
|
||||||
|
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
ref = 'remote-branch-1'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledTimes(1)
|
||||||
|
expect(git.branchDelete).toHaveBeenCalledWith(
|
||||||
|
true,
|
||||||
|
'origin/remote-branch-1/conflict'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function setup(testName: string): Promise<void> {
|
||||||
|
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
|
||||||
|
|
||||||
|
// Repository directory
|
||||||
|
repositoryPath = path.join(testWorkspace, testName)
|
||||||
|
await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
|
||||||
|
|
||||||
|
// Repository URL
|
||||||
|
repositoryUrl = 'https://github.com/my-org/my-repo'
|
||||||
|
|
||||||
|
// Clean
|
||||||
|
clean = true
|
||||||
|
|
||||||
|
// Ref
|
||||||
|
ref = ''
|
||||||
|
|
||||||
|
// Git command manager
|
||||||
|
git = {
|
||||||
|
branchDelete: jest.fn(),
|
||||||
|
branchExists: jest.fn(),
|
||||||
|
branchList: jest.fn(async () => {
|
||||||
|
return []
|
||||||
|
}),
|
||||||
|
checkout: jest.fn(),
|
||||||
|
checkoutDetach: jest.fn(),
|
||||||
|
config: jest.fn(),
|
||||||
|
configExists: jest.fn(),
|
||||||
|
fetch: jest.fn(),
|
||||||
|
getDefaultBranch: jest.fn(),
|
||||||
|
getWorkingDirectory: jest.fn(() => repositoryPath),
|
||||||
|
init: jest.fn(),
|
||||||
|
isDetached: jest.fn(),
|
||||||
|
lfsFetch: jest.fn(),
|
||||||
|
lfsInstall: jest.fn(),
|
||||||
|
log1: jest.fn(),
|
||||||
|
remoteAdd: jest.fn(),
|
||||||
|
removeEnvironmentVariable: jest.fn(),
|
||||||
|
revParse: jest.fn(),
|
||||||
|
setEnvironmentVariable: jest.fn(),
|
||||||
|
shaExists: jest.fn(),
|
||||||
|
submoduleForeach: jest.fn(),
|
||||||
|
submoduleSync: jest.fn(),
|
||||||
|
submoduleUpdate: jest.fn(),
|
||||||
|
tagExists: jest.fn(),
|
||||||
|
tryClean: jest.fn(async () => {
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
tryConfigUnset: jest.fn(),
|
||||||
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
|
tryGetFetchUrl: jest.fn(async () => {
|
||||||
|
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
||||||
|
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
||||||
|
return repositoryUrl
|
||||||
|
}),
|
||||||
|
tryReset: jest.fn(async () => {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-false/regular-file.txt" ]; then
|
||||||
|
echo "Expected regular file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "./submodules-false/submodule-level-1/submodule-file.txt" ]; then
|
||||||
|
echo "Unexpected submodule file exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then
|
|
||||||
echo "Expected regular file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then
|
|
||||||
echo "Unexpected submodule file exists"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-recursive/regular-file.txt" ]; then
|
||||||
|
echo "Expected regular file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-file.txt" ]; then
|
||||||
|
echo "Expected submodule file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
|
||||||
|
echo "Expected nested submodule file does not exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing persisted credential"
|
||||||
|
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
||||||
|
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
||||||
|
if [ "$?" != "0" ]; then
|
||||||
|
echo "Failed to validate persisted credential"
|
||||||
|
popd
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
popd
|
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-true/regular-file.txt" ]; then
|
||||||
|
echo "Expected regular file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-true/submodule-level-1/submodule-file.txt" ]; then
|
||||||
|
echo "Expected submodule file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "./submodules-true/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
|
||||||
|
echo "Unexpected nested submodule file exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing persisted credential"
|
||||||
|
pushd ./submodules-true/submodule-level-1
|
||||||
|
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
||||||
|
if [ "$?" != "0" ]; then
|
||||||
|
echo "Failed to validate persisted credential"
|
||||||
|
popd
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
popd
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,350 @@
|
|||||||
|
import * as assert from 'assert'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as exec from '@actions/exec'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as regexpHelper from './regexp-helper'
|
||||||
|
import * as stateHelper from './state-helper'
|
||||||
|
import * as urlHelper from './url-helper'
|
||||||
|
import {default as uuid} from 'uuid/v4'
|
||||||
|
import {IGitCommandManager} from './git-command-manager'
|
||||||
|
import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
const SSH_COMMAND_KEY = 'core.sshCommand'
|
||||||
|
|
||||||
|
export interface IGitAuthHelper {
|
||||||
|
configureAuth(): Promise<void>
|
||||||
|
configureGlobalAuth(): Promise<void>
|
||||||
|
configureSubmoduleAuth(): Promise<void>
|
||||||
|
removeAuth(): Promise<void>
|
||||||
|
removeGlobalAuth(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAuthHelper(
|
||||||
|
git: IGitCommandManager,
|
||||||
|
settings?: IGitSourceSettings
|
||||||
|
): IGitAuthHelper {
|
||||||
|
return new GitAuthHelper(git, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitAuthHelper {
|
||||||
|
private readonly git: IGitCommandManager
|
||||||
|
private readonly settings: IGitSourceSettings
|
||||||
|
private readonly tokenConfigKey: string
|
||||||
|
private readonly tokenConfigValue: string
|
||||||
|
private readonly tokenPlaceholderConfigValue: string
|
||||||
|
private readonly insteadOfKey: string
|
||||||
|
private readonly insteadOfValue: string
|
||||||
|
private sshCommand = ''
|
||||||
|
private sshKeyPath = ''
|
||||||
|
private sshKnownHostsPath = ''
|
||||||
|
private temporaryHomePath = ''
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
gitCommandManager: IGitCommandManager,
|
||||||
|
gitSourceSettings?: IGitSourceSettings
|
||||||
|
) {
|
||||||
|
this.git = gitCommandManager
|
||||||
|
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)
|
||||||
|
|
||||||
|
// Token auth header
|
||||||
|
const serverUrl = urlHelper.getServerUrl()
|
||||||
|
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
|
||||||
|
const basicCredential = Buffer.from(
|
||||||
|
`x-access-token:${this.settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
core.setSecret(basicCredential)
|
||||||
|
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
|
||||||
|
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
|
||||||
|
// Instead of SSH URL
|
||||||
|
this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf` // "origin" is SCHEME://HOSTNAME[:PORT]
|
||||||
|
this.insteadOfValue = `git@${serverUrl.hostname}:`
|
||||||
|
}
|
||||||
|
|
||||||
|
async configureAuth(): Promise<void> {
|
||||||
|
// Remove possible previous values
|
||||||
|
await this.removeAuth()
|
||||||
|
|
||||||
|
// Configure new values
|
||||||
|
await this.configureSsh()
|
||||||
|
await this.configureToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
async configureGlobalAuth(): Promise<void> {
|
||||||
|
// Create a temp home directory
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
|
const uniqueId = uuid()
|
||||||
|
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
|
||||||
|
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
|
||||||
|
|
||||||
|
// Copy the global git config
|
||||||
|
const gitConfigPath = path.join(
|
||||||
|
process.env['HOME'] || os.homedir(),
|
||||||
|
'.gitconfig'
|
||||||
|
)
|
||||||
|
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
|
||||||
|
let configExists = false
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(gitConfigPath)
|
||||||
|
configExists = true
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configExists) {
|
||||||
|
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
|
||||||
|
await io.cp(gitConfigPath, newGitConfigPath)
|
||||||
|
} else {
|
||||||
|
await fs.promises.writeFile(newGitConfigPath, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Override HOME
|
||||||
|
core.info(
|
||||||
|
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
|
||||||
|
)
|
||||||
|
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
|
||||||
|
|
||||||
|
// Configure the token
|
||||||
|
await this.configureToken(newGitConfigPath, true)
|
||||||
|
|
||||||
|
// Configure HTTPS instead of SSH
|
||||||
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||||
|
if (!this.settings.sshKey) {
|
||||||
|
await this.git.config(this.insteadOfKey, this.insteadOfValue, true)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Unset in case somehow written to the real global config
|
||||||
|
core.info(
|
||||||
|
'Encountered an error when attempting to configure token. Attempting unconfigure.'
|
||||||
|
)
|
||||||
|
await this.git.tryConfigUnset(this.tokenConfigKey, true)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async configureSubmoduleAuth(): Promise<void> {
|
||||||
|
// Remove possible previous HTTPS instead of SSH
|
||||||
|
await this.removeGitConfig(this.insteadOfKey, true)
|
||||||
|
|
||||||
|
if (this.settings.persistCredentials) {
|
||||||
|
// Configure a placeholder value. This approach avoids the credential being captured
|
||||||
|
// by process creation audit events, which are commonly logged. For more information,
|
||||||
|
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
|
const output = await 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replace the placeholder
|
||||||
|
const configPaths: string[] =
|
||||||
|
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||||
|
for (const configPath of configPaths) {
|
||||||
|
core.debug(`Replacing token placeholder in '${configPath}'`)
|
||||||
|
this.replaceTokenPlaceholder(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`,
|
||||||
|
this.settings.nestedSubmodules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAuth(): Promise<void> {
|
||||||
|
await this.removeSsh()
|
||||||
|
await this.removeToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGlobalAuth(): Promise<void> {
|
||||||
|
core.debug(`Unsetting HOME override`)
|
||||||
|
this.git.removeEnvironmentVariable('HOME')
|
||||||
|
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)
|
||||||
|
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
||||||
|
this.sshKeyPath
|
||||||
|
)}"`
|
||||||
|
if (this.settings.sshStrict) {
|
||||||
|
this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
|
||||||
|
}
|
||||||
|
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
||||||
|
this.sshKnownHostsPath
|
||||||
|
)}"`
|
||||||
|
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
|
||||||
|
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
|
||||||
|
|
||||||
|
// Configure core.sshCommand
|
||||||
|
if (this.settings.persistCredentials) {
|
||||||
|
await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async configureToken(
|
||||||
|
configPath?: string,
|
||||||
|
globalConfig?: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
// Validate args
|
||||||
|
assert.ok(
|
||||||
|
(configPath && globalConfig) || (!configPath && !globalConfig),
|
||||||
|
'Unexpected configureToken parameter combinations'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default config path
|
||||||
|
if (!configPath && !globalConfig) {
|
||||||
|
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure a placeholder value. This approach avoids the credential being captured
|
||||||
|
// by process creation audit events, which are commonly logged. For more information,
|
||||||
|
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
|
await this.git.config(
|
||||||
|
this.tokenConfigKey,
|
||||||
|
this.tokenPlaceholderConfigValue,
|
||||||
|
globalConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replace the placeholder
|
||||||
|
await this.replaceTokenPlaceholder(configPath || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
|
||||||
|
assert.ok(configPath, 'configPath is not defined')
|
||||||
|
let content = (await fs.promises.readFile(configPath)).toString()
|
||||||
|
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
||||||
|
if (
|
||||||
|
placeholderIndex < 0 ||
|
||||||
|
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
||||||
|
) {
|
||||||
|
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
||||||
|
}
|
||||||
|
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||||
|
content = content.replace(
|
||||||
|
this.tokenPlaceholderConfigValue,
|
||||||
|
this.tokenConfigValue
|
||||||
|
)
|
||||||
|
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> {
|
||||||
|
// HTTP extra header
|
||||||
|
await this.removeGitConfig(this.tokenConfigKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeGitConfig(
|
||||||
|
configKey: string,
|
||||||
|
submoduleOnly: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
if (!submoduleOnly) {
|
||||||
|
if (
|
||||||
|
(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)
|
||||||
|
await this.git.submoduleForeach(
|
||||||
|
`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
import * as assert from 'assert'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as fsHelper from './fs-helper'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as path from 'path'
|
||||||
|
import {IGitCommandManager} from './git-command-manager'
|
||||||
|
|
||||||
|
export async function prepareExistingDirectory(
|
||||||
|
git: IGitCommandManager | undefined,
|
||||||
|
repositoryPath: string,
|
||||||
|
repositoryUrl: string,
|
||||||
|
clean: boolean,
|
||||||
|
ref: string
|
||||||
|
): Promise<void> {
|
||||||
|
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
|
||||||
|
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
|
||||||
|
|
||||||
|
// Indicates whether to delete the directory contents
|
||||||
|
let remove = false
|
||||||
|
|
||||||
|
// Check whether using git or REST API
|
||||||
|
if (!git) {
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
// Fetch URL does not match
|
||||||
|
else if (
|
||||||
|
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
|
||||||
|
repositoryUrl !== (await git.tryGetFetchUrl())
|
||||||
|
) {
|
||||||
|
remove = true
|
||||||
|
} else {
|
||||||
|
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
|
||||||
|
const lockPaths = [
|
||||||
|
path.join(repositoryPath, '.git', 'index.lock'),
|
||||||
|
path.join(repositoryPath, '.git', 'shallow.lock')
|
||||||
|
]
|
||||||
|
for (const lockPath of lockPaths) {
|
||||||
|
try {
|
||||||
|
await io.rmRF(lockPath)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.startGroup('Removing previously created refs, to avoid conflicts')
|
||||||
|
// Checkout detached HEAD
|
||||||
|
if (!(await git.isDetached())) {
|
||||||
|
await git.checkoutDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all refs/heads/*
|
||||||
|
let branches = await git.branchList(false)
|
||||||
|
for (const branch of branches) {
|
||||||
|
await git.branchDelete(false, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any conflicting refs/remotes/origin/*
|
||||||
|
// Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar
|
||||||
|
// Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo
|
||||||
|
if (ref) {
|
||||||
|
ref = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`
|
||||||
|
if (ref.startsWith('refs/heads/')) {
|
||||||
|
const upperName1 = ref.toUpperCase().substr('REFS/HEADS/'.length)
|
||||||
|
const upperName1Slash = `${upperName1}/`
|
||||||
|
branches = await git.branchList(true)
|
||||||
|
for (const branch of branches) {
|
||||||
|
const upperName2 = branch.substr('origin/'.length).toUpperCase()
|
||||||
|
const upperName2Slash = `${upperName2}/`
|
||||||
|
if (
|
||||||
|
upperName1.startsWith(upperName2Slash) ||
|
||||||
|
upperName2.startsWith(upperName1Slash)
|
||||||
|
) {
|
||||||
|
await git.branchDelete(true, branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
|
// Clean
|
||||||
|
if (clean) {
|
||||||
|
core.startGroup('Cleaning the repository')
|
||||||
|
if (!(await git.tryClean())) {
|
||||||
|
core.debug(
|
||||||
|
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
|
||||||
|
)
|
||||||
|
remove = true
|
||||||
|
} else if (!(await git.tryReset())) {
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
|
if (remove) {
|
||||||
|
core.warning(
|
||||||
|
`Unable to clean or reset the repository. The repository will be recreated instead.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(
|
||||||
|
`Unable to prepare the existing repository. The repository will be recreated instead.`
|
||||||
|
)
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove) {
|
||||||
|
// Delete the contents of the directory. Don't delete the directory itself
|
||||||
|
// since it might be the current working directory.
|
||||||
|
core.info(`Deleting the contents of '${repositoryPath}'`)
|
||||||
|
for (const file of await fs.promises.readdir(repositoryPath)) {
|
||||||
|
await io.rmRF(path.join(repositoryPath, file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
export interface IGitSourceSettings {
|
||||||
|
/**
|
||||||
|
* The location on disk where the repository will be placed
|
||||||
|
*/
|
||||||
|
repositoryPath: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repository owner
|
||||||
|
*/
|
||||||
|
repositoryOwner: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repository name
|
||||||
|
*/
|
||||||
|
repositoryName: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ref to fetch
|
||||||
|
*/
|
||||||
|
ref: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The commit to checkout
|
||||||
|
*/
|
||||||
|
commit: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to clean the repository
|
||||||
|
*/
|
||||||
|
clean: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The depth when fetching
|
||||||
|
*/
|
||||||
|
fetchDepth: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to fetch LFS objects
|
||||||
|
*/
|
||||||
|
lfs: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to checkout submodules
|
||||||
|
*/
|
||||||
|
submodules: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to recursively checkout submodules
|
||||||
|
*/
|
||||||
|
nestedSubmodules: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth token to use when fetching the repository
|
||||||
|
*/
|
||||||
|
authToken: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SSH key to configure
|
||||||
|
*/
|
||||||
|
sshKey: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional SSH known hosts
|
||||||
|
*/
|
||||||
|
sshKnownHosts: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the server must be a known host
|
||||||
|
*/
|
||||||
|
sshStrict: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to persist the credentials on disk to enable scripting authenticated git commands
|
||||||
|
*/
|
||||||
|
persistCredentials: boolean
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export function escape(value: string): string {
|
||||||
|
return value.replace(/[^a-zA-Z0-9_]/g, x => {
|
||||||
|
return `\\${x}`
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import * as assert from 'assert'
|
||||||
|
import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
import {URL} from 'url'
|
||||||
|
|
||||||
|
export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||||
|
assert.ok(
|
||||||
|
settings.repositoryOwner,
|
||||||
|
'settings.repositoryOwner must be defined'
|
||||||
|
)
|
||||||
|
assert.ok(settings.repositoryName, 'settings.repositoryName must be defined')
|
||||||
|
const serviceUrl = getServerUrl()
|
||||||
|
const encodedOwner = encodeURIComponent(settings.repositoryOwner)
|
||||||
|
const encodedName = encodeURIComponent(settings.repositoryName)
|
||||||
|
if (settings.sshKey) {
|
||||||
|
return `git@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
|
||||||
|
}
|
||||||
|
|
||||||
|
// "origin" is SCHEME://HOSTNAME[:PORT]
|
||||||
|
return `${serviceUrl.origin}/${encodedOwner}/${encodedName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerUrl(): URL {
|
||||||
|
// todo: remove GITHUB_URL after support for GHES Alpha is no longer needed
|
||||||
|
return new URL(
|
||||||
|
process.env['GITHUB_SERVER_URL'] ||
|
||||||
|
process.env['GITHUB_URL'] ||
|
||||||
|
'https://github.com'
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue