mirror of
https://github.com/actions/checkout.git
synced 2025-11-07 05:26:55 +00:00
Compare commits
4 commits
527133ddb9
...
3fec15de62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fec15de62 | ||
|
|
71cf2267d8 | ||
|
|
069c695914 | ||
|
|
e7667abffb |
12 changed files with 1084 additions and 151 deletions
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
|
|
@ -302,12 +302,15 @@ jobs:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.6
|
||||||
|
with:
|
||||||
|
path: actions-checkout
|
||||||
|
|
||||||
# Basic checkout using git
|
# Basic checkout using git
|
||||||
- name: Checkout basic
|
- name: Checkout basic
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: ./
|
uses: ./actions-checkout
|
||||||
with:
|
with:
|
||||||
|
path: cloned-using-local-action
|
||||||
ref: test-data/v2/basic
|
ref: test-data/v2/basic
|
||||||
|
|
||||||
# Verify output
|
# Verify output
|
||||||
|
|
@ -325,7 +328,3 @@ jobs:
|
||||||
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# needed to make checkout post cleanup succeed
|
|
||||||
- name: Fix Checkout
|
|
||||||
uses: actions/checkout@v4.1.6
|
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -1,6 +1,13 @@
|
||||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||||
|
|
||||||
# Checkout V5
|
# Checkout v6-beta
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
|
||||||
|
- This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
|
||||||
|
|
||||||
|
# Checkout v5
|
||||||
|
|
||||||
## What's new
|
## What's new
|
||||||
|
|
||||||
|
|
@ -8,7 +15,7 @@
|
||||||
- This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
|
- This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
|
||||||
|
|
||||||
|
|
||||||
# Checkout V4
|
# Checkout v4
|
||||||
|
|
||||||
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
// Assert config
|
// Assert config - check that .git/config contains includeIf entries
|
||||||
const configContent = (
|
const localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Assert credentials config file contains the actual credentials
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
const basicCredential = Buffer.from(
|
const basicCredential = Buffer.from(
|
||||||
`x-access-token:${settings.authToken}`,
|
`x-access-token:${settings.authToken}`,
|
||||||
'utf8'
|
'utf8'
|
||||||
).toString('base64')
|
).toString('base64')
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
|
||||||
'inject https://github.com as github server url'
|
'inject https://github.com as github server url'
|
||||||
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
||||||
await testAuthHeader(
|
await testAuthHeader(
|
||||||
configureAuth_AcceptsGitHubServerUrl,
|
configureAuth_AcceptsGitHubServerUrlSetToGHEC,
|
||||||
'https://github.com'
|
'https://github.com'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
// Assert config
|
// Assert config - check credentials config file (not local .git/config)
|
||||||
const configContent = (
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION`
|
`http.https://github.com/.extraheader AUTHORIZATION`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
|
||||||
expectedSshCommand
|
expectedSshCommand
|
||||||
)
|
)
|
||||||
|
|
||||||
// Asserty git config
|
// Assert git config
|
||||||
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
||||||
.toString()
|
.toString()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(x => x)
|
.filter(x => x)
|
||||||
expect(gitConfigLines).toHaveLength(1)
|
// Should have includeIf entries pointing to credentials file
|
||||||
expect(gitConfigLines[0]).toMatch(/^http\./)
|
expect(gitConfigLines.length).toBeGreaterThan(0)
|
||||||
|
expect(
|
||||||
|
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
|
||||||
|
).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
||||||
|
|
@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf('value-from-global-config')
|
configContent.indexOf('value-from-global-config')
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
// Global config should have include.path pointing to credentials file
|
||||||
|
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Check credentials in the separate config file
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
|
||||||
const configContent = (
|
const configContent = (
|
||||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
).toString()
|
).toString()
|
||||||
|
// Global config should have include.path pointing to credentials file
|
||||||
|
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Check credentials in the separate config file
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
|
// Should configure insteadOf (2 calls for two values)
|
||||||
|
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||||
/unset-all.*insteadOf/
|
/unset-all.*insteadOf/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
|
||||||
/url.*insteadOf.*git@github.com:/
|
/url.*insteadOf.*git@github.com:/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||||
/url.*insteadOf.*org-123456@github.com:/
|
/url.*insteadOf.*org-123456@github.com:/
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
// Should configure sshCommand (1 call)
|
||||||
|
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||||
/unset-all.*insteadOf/
|
/unset-all.*insteadOf/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
|
||||||
await setup(removeAuth_removesToken)
|
await setup(removeAuth_removesToken)
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
let gitConfigContent = (
|
|
||||||
|
// Verify includeIf entries exist in local config
|
||||||
|
let localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Verify both host and container includeIf entries are present
|
||||||
|
const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Verify credentials file exists
|
||||||
|
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
|
||||||
|
// Verify credentials file contains the auth token
|
||||||
|
let credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsFilePath)
|
||||||
|
).toString()
|
||||||
|
const basicCredential = Buffer.from(
|
||||||
|
`x-access-token:${settings.authToken}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64')
|
||||||
|
expect(
|
||||||
|
credentialsContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Verify the includeIf entries point to the credentials file
|
||||||
|
const containerCredentialsPath = path.posix.join(
|
||||||
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsFilePath)
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(credentialsFilePath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.removeAuth()
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
// Assert git config
|
// Assert all includeIf entries removed from local git config
|
||||||
gitConfigContent = (
|
localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
|
||||||
|
).toBeLessThan(0)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
|
||||||
|
).toBeLessThan(0)
|
||||||
|
expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||||
|
expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||||
|
|
||||||
|
// Assert credentials config file deleted
|
||||||
|
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(0)
|
||||||
|
|
||||||
|
// Verify credentials file no longer exists on disk
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(credentialsFilePath)
|
||||||
|
throw new Error('Credentials file should have been deleted')
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as any)?.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeAuth_removesTokenFromSubmodules =
|
||||||
|
'removeAuth removes token from submodules'
|
||||||
|
it(removeAuth_removesTokenFromSubmodules, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(removeAuth_removesTokenFromSubmodules)
|
||||||
|
|
||||||
|
// Create fake submodule config paths
|
||||||
|
const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
|
||||||
|
const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
|
||||||
|
const submodule1ConfigPath = path.join(submodule1Dir, 'config')
|
||||||
|
const submodule2ConfigPath = path.join(submodule2Dir, 'config')
|
||||||
|
|
||||||
|
await fs.promises.mkdir(submodule1Dir, {recursive: true})
|
||||||
|
await fs.promises.mkdir(submodule2Dir, {recursive: true})
|
||||||
|
await fs.promises.writeFile(submodule1ConfigPath, '')
|
||||||
|
await fs.promises.writeFile(submodule2ConfigPath, '')
|
||||||
|
|
||||||
|
// Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
|
||||||
|
const mockGetSubmoduleConfigPaths =
|
||||||
|
git.getSubmoduleConfigPaths as jest.Mock<any, any>
|
||||||
|
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||||
|
submodule1ConfigPath,
|
||||||
|
submodule2ConfigPath
|
||||||
|
])
|
||||||
|
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
|
// Verify credentials file exists
|
||||||
|
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
|
||||||
|
// Verify submodule 1 config has includeIf entries
|
||||||
|
let submodule1Content = (
|
||||||
|
await fs.promises.readFile(submodule1ConfigPath)
|
||||||
|
).toString()
|
||||||
|
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
|
||||||
|
expect(
|
||||||
|
submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule1Content.indexOf(credentialsFilePath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Verify submodule 2 config has includeIf entries
|
||||||
|
let submodule2Content = (
|
||||||
|
await fs.promises.readFile(submodule2ConfigPath)
|
||||||
|
).toString()
|
||||||
|
const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(credentialsFilePath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Verify both host and container paths are in each submodule config
|
||||||
|
const containerCredentialsPath = path.posix.join(
|
||||||
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsFilePath)
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
submodule1Content.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Act - ensure mock persists for removeAuth
|
||||||
|
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||||
|
submodule1ConfigPath,
|
||||||
|
submodule2ConfigPath
|
||||||
|
])
|
||||||
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
|
// Assert submodule 1 includeIf entries removed
|
||||||
|
submodule1Content = (
|
||||||
|
await fs.promises.readFile(submodule1ConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||||
|
expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||||
|
expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||||
|
|
||||||
|
// Assert submodule 2 includeIf entries removed
|
||||||
|
submodule2Content = (
|
||||||
|
await fs.promises.readFile(submodule2ConfigPath)
|
||||||
|
).toString()
|
||||||
|
expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||||
|
expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||||
|
expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||||
|
|
||||||
|
// Assert credentials config file deleted
|
||||||
|
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(0)
|
||||||
|
|
||||||
|
// Verify credentials file no longer exists on disk
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(credentialsFilePath)
|
||||||
|
throw new Error('Credentials file should have been deleted')
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as any)?.code !== 'ENOENT') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeGlobalConfig_removesOverride =
|
const removeGlobalConfig_removesOverride =
|
||||||
|
|
@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const testCredentialsConfigPath_matchesCredentialsConfigPaths =
|
||||||
|
'testCredentialsConfigPath matches credentials config paths'
|
||||||
|
it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Get a real credentials config path
|
||||||
|
const credentialsConfigPath = await (
|
||||||
|
authHelper as any
|
||||||
|
).getCredentialsConfigPath()
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
// Test invalid paths
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/other-config.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-invalid.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function setup(testName: string): Promise<void> {
|
async function setup(testName: string): Promise<void> {
|
||||||
|
|
@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
|
||||||
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
||||||
process.env['RUNNER_TEMP'] = runnerTemp
|
process.env['RUNNER_TEMP'] = runnerTemp
|
||||||
process.env['HOME'] = tempHomedir
|
process.env['HOME'] = tempHomedir
|
||||||
|
process.env['GITHUB_WORKSPACE'] = workspace
|
||||||
|
|
||||||
// Create git config
|
// Create git config
|
||||||
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
||||||
|
|
@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
|
||||||
checkout: jest.fn(),
|
checkout: jest.fn(),
|
||||||
checkoutDetach: jest.fn(),
|
checkoutDetach: jest.fn(),
|
||||||
config: jest.fn(
|
config: jest.fn(
|
||||||
async (key: string, value: string, globalConfig?: boolean) => {
|
async (
|
||||||
const configPath = globalConfig
|
key: string,
|
||||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
value: string,
|
||||||
: localGitConfigPath
|
globalConfig?: boolean,
|
||||||
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
) => {
|
||||||
|
const configPath =
|
||||||
|
configFile ||
|
||||||
|
(globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath)
|
||||||
|
// Ensure directory exists
|
||||||
|
await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
|
||||||
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
|
||||||
env: {},
|
env: {},
|
||||||
fetch: jest.fn(),
|
fetch: jest.fn(),
|
||||||
getDefaultBranch: jest.fn(),
|
getDefaultBranch: jest.fn(),
|
||||||
|
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||||
getWorkingDirectory: jest.fn(() => workspace),
|
getWorkingDirectory: jest.fn(() => workspace),
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
isDetached: jest.fn(),
|
isDetached: jest.fn(),
|
||||||
|
|
@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
tryConfigUnsetValue: jest.fn(
|
||||||
|
async (
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configPath?: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const targetConfigPath =
|
||||||
|
configPath ||
|
||||||
|
(globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath)
|
||||||
|
let content = await fs.promises.readFile(targetConfigPath)
|
||||||
|
let lines = content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x)
|
||||||
|
.filter(x => !(x.startsWith(key) && x.includes(value)))
|
||||||
|
await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
),
|
||||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
tryGetFetchUrl: jest.fn(),
|
tryGetFetchUrl: jest.fn(),
|
||||||
|
tryGetConfigValues: jest.fn(
|
||||||
|
async (
|
||||||
|
key: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configPath?: string
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const targetConfigPath =
|
||||||
|
configPath ||
|
||||||
|
(globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath)
|
||||||
|
const content = await fs.promises.readFile(targetConfigPath)
|
||||||
|
const lines = content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x && x.startsWith(key))
|
||||||
|
.map(x => x.substring(key.length).trim())
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
),
|
||||||
|
tryGetConfigKeys: jest.fn(
|
||||||
|
async (
|
||||||
|
pattern: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configPath?: string
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const targetConfigPath =
|
||||||
|
configPath ||
|
||||||
|
(globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath)
|
||||||
|
const content = await fs.promises.readFile(targetConfigPath)
|
||||||
|
const lines = content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x)
|
||||||
|
const keys = lines
|
||||||
|
.filter(x => new RegExp(pattern).test(x.split(' ')[0]))
|
||||||
|
.map(x => x.split(' ')[0])
|
||||||
|
return [...new Set(keys)] // Remove duplicates
|
||||||
|
}
|
||||||
|
),
|
||||||
tryReset: jest.fn(),
|
tryReset: jest.fn(),
|
||||||
version: jest.fn()
|
version: jest.fn()
|
||||||
}
|
}
|
||||||
|
|
@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
|
||||||
|
|
||||||
async function getActualSshKeyPath(): Promise<string> {
|
async function getActualSshKeyPath(): Promise<string> {
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||||
.sort()
|
.sort()
|
||||||
.map(x => path.join(runnerTemp, x))
|
.map(x => path.join(runnerTemp, x))
|
||||||
if (actualTempFiles.length === 0) {
|
if (actualTempFiles.length === 0) {
|
||||||
|
|
@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
|
||||||
|
|
||||||
async function getActualSshKnownHostsPath(): Promise<string> {
|
async function getActualSshKnownHostsPath(): Promise<string> {
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||||
.sort()
|
.sort()
|
||||||
.map(x => path.join(runnerTemp, x))
|
.map(x => path.join(runnerTemp, x))
|
||||||
if (actualTempFiles.length === 0) {
|
if (actualTempFiles.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
|
||||||
configExists: jest.fn(),
|
configExists: jest.fn(),
|
||||||
fetch: jest.fn(),
|
fetch: jest.fn(),
|
||||||
getDefaultBranch: jest.fn(),
|
getDefaultBranch: jest.fn(),
|
||||||
|
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||||
getWorkingDirectory: jest.fn(() => repositoryPath),
|
getWorkingDirectory: jest.fn(() => repositoryPath),
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
isDetached: jest.fn(),
|
isDetached: jest.fn(),
|
||||||
|
|
@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
tryConfigUnset: jest.fn(),
|
tryConfigUnset: jest.fn(),
|
||||||
|
tryConfigUnsetValue: jest.fn(),
|
||||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
tryGetFetchUrl: jest.fn(async () => {
|
tryGetFetchUrl: jest.fn(async () => {
|
||||||
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
||||||
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
||||||
return repositoryUrl
|
return repositoryUrl
|
||||||
}),
|
}),
|
||||||
|
tryGetConfigValues: jest.fn(),
|
||||||
|
tryGetConfigKeys: jest.fn(),
|
||||||
tryReset: jest.fn(async () => {
|
tryReset: jest.fn(async () => {
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ fi
|
||||||
|
|
||||||
echo "Testing persisted credential"
|
echo "Testing persisted credential"
|
||||||
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo "Failed to validate persisted credential"
|
echo "Failed to validate persisted credential"
|
||||||
popd
|
popd
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ fi
|
||||||
|
|
||||||
echo "Testing persisted credential"
|
echo "Testing persisted credential"
|
||||||
pushd ./submodules-true/submodule-level-1
|
pushd ./submodules-true/submodule-level-1
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo "Failed to validate persisted credential"
|
echo "Failed to validate persisted credential"
|
||||||
popd
|
popd
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,10 @@ inputs:
|
||||||
github-server-url:
|
github-server-url:
|
||||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||||
required: false
|
required: false
|
||||||
|
skip-cleanup:
|
||||||
|
description: Skips the cleanup phase on post action hook
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
outputs:
|
outputs:
|
||||||
ref:
|
ref:
|
||||||
description: 'The branch, tag or SHA that was checked out'
|
description: 'The branch, tag or SHA that was checked out'
|
||||||
|
|
|
||||||
338
dist/index.js
vendored
338
dist/index.js
vendored
|
|
@ -162,6 +162,7 @@ class GitAuthHelper {
|
||||||
this.sshKeyPath = '';
|
this.sshKeyPath = '';
|
||||||
this.sshKnownHostsPath = '';
|
this.sshKnownHostsPath = '';
|
||||||
this.temporaryHomePath = '';
|
this.temporaryHomePath = '';
|
||||||
|
this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
|
||||||
this.git = gitCommandManager;
|
this.git = gitCommandManager;
|
||||||
this.settings = gitSourceSettings || {};
|
this.settings = gitSourceSettings || {};
|
||||||
// Token auth header
|
// Token auth header
|
||||||
|
|
@ -229,15 +230,17 @@ class GitAuthHelper {
|
||||||
configureGlobalAuth() {
|
configureGlobalAuth() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||||
const newGitConfigPath = yield this.configureTempGlobalConfig();
|
yield this.configureTempGlobalConfig();
|
||||||
try {
|
try {
|
||||||
// Configure the token
|
// Configure the token
|
||||||
yield this.configureToken(newGitConfigPath, true);
|
yield this.configureToken(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);
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
for (const insteadOfValue of this.insteadOfValues) {
|
for (const insteadOfValue of this.insteadOfValues) {
|
||||||
yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
|
yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
|
||||||
|
true // add?
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,19 +255,34 @@ 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
|
// Remove possible previous HTTPS instead of SSH
|
||||||
yield this.removeGitConfig(this.insteadOfKey, true);
|
yield this.removeSubmoduleGitConfig(this.insteadOfKey);
|
||||||
if (this.settings.persistCredentials) {
|
if (this.settings.persistCredentials) {
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
const credentialsConfigPath = this.getCredentialsConfigPath();
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
// Container credentials config path
|
||||||
const output = yield this.git.submoduleForeach(
|
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
// Get submodule config file paths.
|
||||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
|
const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
|
||||||
// Replace the placeholder
|
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||||
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
|
// Configure both host and container paths to support Docker container actions.
|
||||||
for (const configPath of configPaths) {
|
for (const configPath of configPaths) {
|
||||||
core.debug(`Replacing token placeholder in '${configPath}'`);
|
// Submodule Git directory
|
||||||
yield this.replaceTokenPlaceholder(configPath);
|
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
|
||||||
|
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
// Configure host includeIf
|
||||||
|
yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath);
|
||||||
|
// Container submodule git directory
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
|
||||||
|
let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
|
||||||
|
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
|
||||||
|
// Configure container includeIf
|
||||||
|
yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath);
|
||||||
}
|
}
|
||||||
if (this.settings.sshKey) {
|
if (this.settings.sshKey) {
|
||||||
// Configure core.sshCommand
|
// Configure core.sshCommand
|
||||||
|
|
@ -295,6 +313,10 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Configures SSH authentication by writing the SSH key and known hosts,
|
||||||
|
* and setting up the GIT_SSH_COMMAND environment variable.
|
||||||
|
*/
|
||||||
configureSsh() {
|
configureSsh() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
|
|
@ -351,43 +373,88 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
configureToken(configPath, globalConfig) {
|
/**
|
||||||
|
* Configures token-based authentication by creating a credentials config file
|
||||||
|
* and setting up includeIf entries to reference it.
|
||||||
|
* @param globalConfig Whether to configure global config instead of local
|
||||||
|
*/
|
||||||
|
configureToken(globalConfig) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Validate args
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
|
const credentialsConfigPath = this.getCredentialsConfigPath();
|
||||||
// Default config path
|
// Write placeholder to the separate credentials config file using git config.
|
||||||
if (!configPath && !globalConfig) {
|
// This approach avoids the credential being captured by process creation audit events,
|
||||||
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
|
// 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
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
false, // add?
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
credentialsConfigPath);
|
||||||
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
|
// Replace the placeholder in the credentials config file
|
||||||
// Replace the placeholder
|
let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
|
||||||
yield this.replaceTokenPlaceholder(configPath || '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
replaceTokenPlaceholder(configPath) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
assert.ok(configPath, 'configPath is not defined');
|
|
||||||
let content = (yield fs.promises.readFile(configPath)).toString();
|
|
||||||
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
|
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
|
||||||
if (placeholderIndex < 0 ||
|
if (placeholderIndex < 0 ||
|
||||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
|
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
|
||||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`);
|
throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
|
||||||
}
|
}
|
||||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
|
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
|
||||||
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
|
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
|
||||||
yield fs.promises.writeFile(configPath, content);
|
yield fs.promises.writeFile(credentialsConfigPath, content);
|
||||||
|
// Add include or includeIf to reference the credentials config
|
||||||
|
if (globalConfig) {
|
||||||
|
// Global config file is temporary
|
||||||
|
yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Host git directory
|
||||||
|
let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
|
||||||
|
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
// Configure host includeIf
|
||||||
|
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
|
||||||
|
yield this.git.config(hostIncludeKey, credentialsConfigPath);
|
||||||
|
// Container git directory
|
||||||
|
const workingDirectory = this.git.getWorkingDirectory();
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
|
||||||
|
let relativePath = path.relative(githubWorkspace, workingDirectory);
|
||||||
|
relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
|
const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
|
||||||
|
// Container credentials config path
|
||||||
|
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
|
||||||
|
// Configure container includeIf
|
||||||
|
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
|
||||||
|
yield this.git.config(containerIncludeKey, containerCredentialsPath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||||
|
* @returns The absolute path to the credentials config file
|
||||||
|
*/
|
||||||
|
getCredentialsConfigPath() {
|
||||||
|
if (this.credentialsConfigPath) {
|
||||||
|
return this.credentialsConfigPath;
|
||||||
|
}
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || '';
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
|
||||||
|
// Create a unique filename for this checkout instance
|
||||||
|
const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
|
||||||
|
this.credentialsConfigPath = path.join(runnerTemp, configFileName);
|
||||||
|
core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
|
||||||
|
return this.credentialsConfigPath;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes SSH authentication configuration by cleaning up SSH keys,
|
||||||
|
* known hosts files, and SSH command configurations.
|
||||||
|
*/
|
||||||
removeSsh() {
|
removeSsh() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
var _a;
|
var _a, _b;
|
||||||
// SSH key
|
// SSH key
|
||||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
|
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
|
||||||
if (keyPath) {
|
if (keyPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH key '${keyPath}'`);
|
||||||
yield io.rmRF(keyPath);
|
yield io.rmRF(keyPath);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|
@ -399,37 +466,136 @@ class GitAuthHelper {
|
||||||
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
|
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
|
||||||
if (knownHostsPath) {
|
if (knownHostsPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH known hosts '${knownHostsPath}'`);
|
||||||
yield io.rmRF(knownHostsPath);
|
yield io.rmRF(knownHostsPath);
|
||||||
}
|
}
|
||||||
catch (_b) {
|
catch (err) {
|
||||||
// Intentionally empty
|
core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
||||||
|
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SSH command
|
// SSH command
|
||||||
|
core.info('Removing SSH command configuration');
|
||||||
yield this.removeGitConfig(SSH_COMMAND_KEY);
|
yield this.removeGitConfig(SSH_COMMAND_KEY);
|
||||||
|
yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Removes token-based authentication by cleaning up HTTP headers,
|
||||||
|
* includeIf entries, and credentials config files.
|
||||||
|
*/
|
||||||
removeToken() {
|
removeToken() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// HTTP extra header
|
var _a;
|
||||||
|
// Remove HTTP extra header
|
||||||
|
core.info('Removing HTTP extra header');
|
||||||
yield this.removeGitConfig(this.tokenConfigKey);
|
yield this.removeGitConfig(this.tokenConfigKey);
|
||||||
});
|
yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
|
||||||
}
|
// Collect credentials config paths that need to be removed
|
||||||
removeGitConfig(configKey_1) {
|
const credentialsPaths = new Set();
|
||||||
return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
|
// Remove includeIf entries that point to git-credentials-*.config files
|
||||||
if (!submoduleOnly) {
|
core.info('Removing includeIf entries pointing to credentials config files');
|
||||||
if ((yield this.git.configExists(configKey)) &&
|
const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
|
||||||
!(yield this.git.tryConfigUnset(configKey))) {
|
mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
|
||||||
// Load the config contents
|
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`);
|
const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
|
||||||
|
for (const configPath of submoduleConfigPaths) {
|
||||||
|
const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
|
||||||
|
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
|
||||||
|
}
|
||||||
|
// Remove credentials config files
|
||||||
|
for (const credentialsPath of credentialsPaths) {
|
||||||
|
// Only remove credentials config files if they are under RUNNER_TEMP
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'];
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
|
||||||
|
if (credentialsPath.startsWith(runnerTemp)) {
|
||||||
|
try {
|
||||||
|
core.info(`Removing credentials config '${credentialsPath}'`);
|
||||||
|
yield io.rmRF(credentialsPath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
||||||
|
core.warning(`Failed to remove credentials config '${credentialsPath}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes a git config key from the local repository config.
|
||||||
|
* @param configKey The git config key to remove
|
||||||
|
*/
|
||||||
|
removeGitConfig(configKey) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if ((yield this.git.configExists(configKey)) &&
|
||||||
|
!(yield this.git.tryConfigUnset(configKey))) {
|
||||||
|
// Load the config contents
|
||||||
|
core.warning(`Failed to remove '${configKey}' from the git config`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes a git config key from all submodule configs.
|
||||||
|
* @param configKey The git config key to remove
|
||||||
|
*/
|
||||||
|
removeSubmoduleGitConfig(configKey) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const pattern = regexpHelper.escape(configKey);
|
const pattern = regexpHelper.escape(configKey);
|
||||||
yield this.git.submoduleForeach(
|
yield this.git.submoduleForeach(
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
|
||||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
|
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Removes includeIf entries that point to git-credentials-*.config files.
|
||||||
|
* @param configPath Optional path to a specific git config file to operate on
|
||||||
|
* @returns Array of unique credentials config file paths that were found and removed
|
||||||
|
*/
|
||||||
|
removeIncludeIfCredentials(configPath) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const credentialsPaths = new Set();
|
||||||
|
try {
|
||||||
|
// Get all includeIf.gitdir keys
|
||||||
|
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
|
||||||
|
configPath);
|
||||||
|
for (const key of keys) {
|
||||||
|
// Get all values for this key
|
||||||
|
const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
|
||||||
|
configPath);
|
||||||
|
if (values.length > 0) {
|
||||||
|
// Remove only values that match git-credentials-<uuid>.config pattern
|
||||||
|
for (const value of values) {
|
||||||
|
if (this.testCredentialsConfigPath(value)) {
|
||||||
|
credentialsPaths.add(value);
|
||||||
|
yield this.git.tryConfigUnsetValue(key, value, false, configPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// Ignore errors - this is cleanup code
|
||||||
|
if (configPath) {
|
||||||
|
core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.debug(`Error during includeIf cleanup: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(credentialsPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tests if a path matches the git-credentials-*.config pattern.
|
||||||
|
* @param path The path to test
|
||||||
|
* @returns True if the path matches the credentials config pattern
|
||||||
|
*/
|
||||||
|
testCredentialsConfigPath(path) {
|
||||||
|
return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -627,9 +793,15 @@ class GitCommandManager {
|
||||||
yield this.execGit(args);
|
yield this.execGit(args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
config(configKey, configValue, globalConfig, add) {
|
config(configKey, configValue, globalConfig, add, configFile) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const args = ['config', globalConfig ? '--global' : '--local'];
|
const args = ['config'];
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local');
|
||||||
|
}
|
||||||
if (add) {
|
if (add) {
|
||||||
args.push('--add');
|
args.push('--add');
|
||||||
}
|
}
|
||||||
|
|
@ -706,6 +878,16 @@ class GitCommandManager {
|
||||||
throw new Error('Unexpected output when retrieving default branch');
|
throw new Error('Unexpected output when retrieving default branch');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getSubmoduleConfigPaths(recursive) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Get submodule config file paths.
|
||||||
|
// Use `--show-origin` to get the config file path for each submodule.
|
||||||
|
const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
|
||||||
|
// Extract config file paths from the output (lines starting with "file:").
|
||||||
|
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
|
||||||
|
return configPaths;
|
||||||
|
});
|
||||||
|
}
|
||||||
getWorkingDirectory() {
|
getWorkingDirectory() {
|
||||||
return this.workingDirectory;
|
return this.workingDirectory;
|
||||||
}
|
}
|
||||||
|
|
@ -836,6 +1018,20 @@ class GitCommandManager {
|
||||||
return output.exitCode === 0;
|
return output.exitCode === 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const args = ['config'];
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local');
|
||||||
|
}
|
||||||
|
args.push('--unset', configKey, configValue);
|
||||||
|
const output = yield this.execGit(args, true);
|
||||||
|
return output.exitCode === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
tryDisableAutomaticGarbageCollection() {
|
tryDisableAutomaticGarbageCollection() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
|
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
|
||||||
|
|
@ -855,6 +1051,46 @@ class GitCommandManager {
|
||||||
return stdout;
|
return stdout;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
tryGetConfigValues(configKey, globalConfig, configFile) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const args = ['config'];
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local');
|
||||||
|
}
|
||||||
|
args.push('--get-all', configKey);
|
||||||
|
const output = yield this.execGit(args, true);
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(value => value.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tryGetConfigKeys(pattern, globalConfig, configFile) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const args = ['config'];
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local');
|
||||||
|
}
|
||||||
|
args.push('--name-only', '--get-regexp', pattern);
|
||||||
|
const output = yield this.execGit(args, true);
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(key => key.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
tryReset() {
|
tryReset() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
|
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class GitAuthHelper {
|
||||||
private sshKeyPath = ''
|
private sshKeyPath = ''
|
||||||
private sshKnownHostsPath = ''
|
private sshKnownHostsPath = ''
|
||||||
private temporaryHomePath = ''
|
private temporaryHomePath = ''
|
||||||
|
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gitCommandManager: IGitCommandManager,
|
gitCommandManager: IGitCommandManager,
|
||||||
|
|
@ -126,16 +127,21 @@ class GitAuthHelper {
|
||||||
|
|
||||||
async configureGlobalAuth(): Promise<void> {
|
async configureGlobalAuth(): Promise<void> {
|
||||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
await this.configureTempGlobalConfig()
|
||||||
try {
|
try {
|
||||||
// Configure the token
|
// Configure the token
|
||||||
await this.configureToken(newGitConfigPath, true)
|
await this.configureToken(true)
|
||||||
|
|
||||||
// Configure HTTPS instead of SSH
|
// Configure HTTPS instead of SSH
|
||||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
for (const insteadOfValue of this.insteadOfValues) {
|
for (const insteadOfValue of this.insteadOfValues) {
|
||||||
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
|
await this.git.config(
|
||||||
|
this.insteadOfKey,
|
||||||
|
insteadOfValue,
|
||||||
|
true, // globalConfig?
|
||||||
|
true // add?
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -150,24 +156,60 @@ class GitAuthHelper {
|
||||||
|
|
||||||
async configureSubmoduleAuth(): Promise<void> {
|
async configureSubmoduleAuth(): Promise<void> {
|
||||||
// Remove possible previous HTTPS instead of SSH
|
// Remove possible previous HTTPS instead of SSH
|
||||||
await this.removeGitConfig(this.insteadOfKey, true)
|
await this.removeSubmoduleGitConfig(this.insteadOfKey)
|
||||||
|
|
||||||
if (this.settings.persistCredentials) {
|
if (this.settings.persistCredentials) {
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||||
// 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(
|
// Container credentials config path
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
const containerCredentialsPath = path.posix.join(
|
||||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsConfigPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get submodule config file paths.
|
||||||
|
const configPaths = await this.git.getSubmoduleConfigPaths(
|
||||||
this.settings.nestedSubmodules
|
this.settings.nestedSubmodules
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replace the placeholder
|
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||||
const configPaths: string[] =
|
// Configure both host and container paths to support Docker container actions.
|
||||||
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}'`)
|
// Submodule Git directory
|
||||||
await this.replaceTokenPlaceholder(configPath)
|
let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
|
||||||
|
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
|
// Configure host includeIf
|
||||||
|
await this.git.config(
|
||||||
|
`includeIf.gitdir:${submoduleGitDir}.path`,
|
||||||
|
credentialsConfigPath,
|
||||||
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container submodule git directory
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||||
|
let relativeSubmoduleGitDir = path.relative(
|
||||||
|
githubWorkspace,
|
||||||
|
submoduleGitDir
|
||||||
|
)
|
||||||
|
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
const containerSubmoduleGitDir = path.posix.join(
|
||||||
|
'/github/workspace',
|
||||||
|
relativeSubmoduleGitDir
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure container includeIf
|
||||||
|
await this.git.config(
|
||||||
|
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
|
||||||
|
containerCredentialsPath,
|
||||||
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.sshKey) {
|
if (this.settings.sshKey) {
|
||||||
|
|
@ -201,6 +243,10 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures SSH authentication by writing the SSH key and known hosts,
|
||||||
|
* and setting up the GIT_SSH_COMMAND environment variable.
|
||||||
|
*/
|
||||||
private async configureSsh(): Promise<void> {
|
private async configureSsh(): Promise<void> {
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
return
|
return
|
||||||
|
|
@ -272,57 +318,116 @@ class GitAuthHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async configureToken(
|
/**
|
||||||
configPath?: string,
|
* Configures token-based authentication by creating a credentials config file
|
||||||
globalConfig?: boolean
|
* and setting up includeIf entries to reference it.
|
||||||
): Promise<void> {
|
* @param globalConfig Whether to configure global config instead of local
|
||||||
// Validate args
|
*/
|
||||||
assert.ok(
|
private async configureToken(globalConfig?: boolean): Promise<void> {
|
||||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
'Unexpected configureToken parameter combinations'
|
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||||
)
|
|
||||||
|
|
||||||
// Default config path
|
// Write placeholder to the separate credentials config file using git config.
|
||||||
if (!configPath && !globalConfig) {
|
// This approach avoids the credential being captured by process creation audit events,
|
||||||
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
|
// 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
|
||||||
|
|
||||||
// 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(
|
await this.git.config(
|
||||||
this.tokenConfigKey,
|
this.tokenConfigKey,
|
||||||
this.tokenPlaceholderConfigValue,
|
this.tokenPlaceholderConfigValue,
|
||||||
globalConfig
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
credentialsConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replace the placeholder
|
// Replace the placeholder in the credentials config file
|
||||||
await this.replaceTokenPlaceholder(configPath || '')
|
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
||||||
if (
|
if (
|
||||||
placeholderIndex < 0 ||
|
placeholderIndex < 0 ||
|
||||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
||||||
) {
|
) {
|
||||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
throw new Error(
|
||||||
|
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
this.tokenPlaceholderConfigValue,
|
this.tokenPlaceholderConfigValue,
|
||||||
this.tokenConfigValue
|
this.tokenConfigValue
|
||||||
)
|
)
|
||||||
await fs.promises.writeFile(configPath, content)
|
await fs.promises.writeFile(credentialsConfigPath, content)
|
||||||
|
|
||||||
|
// Add include or includeIf to reference the credentials config
|
||||||
|
if (globalConfig) {
|
||||||
|
// Global config file is temporary
|
||||||
|
await this.git.config(
|
||||||
|
'include.path',
|
||||||
|
credentialsConfigPath,
|
||||||
|
true // globalConfig?
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Host git directory
|
||||||
|
let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
|
||||||
|
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
|
// Configure host includeIf
|
||||||
|
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
|
||||||
|
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
|
// Container git directory
|
||||||
|
const workingDirectory = this.git.getWorkingDirectory()
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||||
|
let relativePath = path.relative(githubWorkspace, workingDirectory)
|
||||||
|
relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
const containerGitDir = path.posix.join(
|
||||||
|
'/github/workspace',
|
||||||
|
relativePath,
|
||||||
|
'.git'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container credentials config path
|
||||||
|
const containerCredentialsPath = path.posix.join(
|
||||||
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsConfigPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure container includeIf
|
||||||
|
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
|
||||||
|
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||||
|
* @returns The absolute path to the credentials config file
|
||||||
|
*/
|
||||||
|
private getCredentialsConfigPath(): string {
|
||||||
|
if (this.credentialsConfigPath) {
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
|
|
||||||
|
// Create a unique filename for this checkout instance
|
||||||
|
const configFileName = `git-credentials-${uuid()}.config`
|
||||||
|
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
|
||||||
|
|
||||||
|
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes SSH authentication configuration by cleaning up SSH keys,
|
||||||
|
* known hosts files, and SSH command configurations.
|
||||||
|
*/
|
||||||
private async removeSsh(): Promise<void> {
|
private async removeSsh(): Promise<void> {
|
||||||
// SSH key
|
// SSH key
|
||||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||||
if (keyPath) {
|
if (keyPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH key '${keyPath}'`)
|
||||||
await io.rmRF(keyPath)
|
await io.rmRF(keyPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
core.debug(`${(err as any)?.message ?? err}`)
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
|
|
@ -335,40 +440,149 @@ class GitAuthHelper {
|
||||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||||
if (knownHostsPath) {
|
if (knownHostsPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
|
||||||
await io.rmRF(knownHostsPath)
|
await io.rmRF(knownHostsPath)
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Intentionally empty
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
|
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSH command
|
// SSH command
|
||||||
|
core.info('Removing SSH command configuration')
|
||||||
await this.removeGitConfig(SSH_COMMAND_KEY)
|
await this.removeGitConfig(SSH_COMMAND_KEY)
|
||||||
|
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes token-based authentication by cleaning up HTTP headers,
|
||||||
|
* includeIf entries, and credentials config files.
|
||||||
|
*/
|
||||||
private async removeToken(): Promise<void> {
|
private async removeToken(): Promise<void> {
|
||||||
// HTTP extra header
|
// Remove HTTP extra header
|
||||||
|
core.info('Removing HTTP extra header')
|
||||||
await this.removeGitConfig(this.tokenConfigKey)
|
await this.removeGitConfig(this.tokenConfigKey)
|
||||||
}
|
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
|
||||||
|
|
||||||
private async removeGitConfig(
|
// Collect credentials config paths that need to be removed
|
||||||
configKey: string,
|
const credentialsPaths = new Set<string>()
|
||||||
submoduleOnly: boolean = false
|
|
||||||
): Promise<void> {
|
// Remove includeIf entries that point to git-credentials-*.config files
|
||||||
if (!submoduleOnly) {
|
core.info('Removing includeIf entries pointing to credentials config files')
|
||||||
if (
|
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||||
(await this.git.configExists(configKey)) &&
|
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||||
!(await this.git.tryConfigUnset(configKey))
|
|
||||||
) {
|
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||||
// Load the config contents
|
const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
for (const configPath of submoduleConfigPaths) {
|
||||||
}
|
const submoduleCredentialsPaths =
|
||||||
|
await this.removeIncludeIfCredentials(configPath)
|
||||||
|
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove credentials config files
|
||||||
|
for (const credentialsPath of credentialsPaths) {
|
||||||
|
// Only remove credentials config files if they are under RUNNER_TEMP
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP']
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
|
if (credentialsPath.startsWith(runnerTemp)) {
|
||||||
|
try {
|
||||||
|
core.info(`Removing credentials config '${credentialsPath}'`)
|
||||||
|
await io.rmRF(credentialsPath)
|
||||||
|
} catch (err) {
|
||||||
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
|
core.warning(
|
||||||
|
`Failed to remove credentials config '${credentialsPath}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
core.debug(
|
||||||
|
`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a git config key from the local repository config.
|
||||||
|
* @param configKey The git config key to remove
|
||||||
|
*/
|
||||||
|
private async removeGitConfig(configKey: string): Promise<void> {
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a git config key from all submodule configs.
|
||||||
|
* @param configKey The git config key to remove
|
||||||
|
*/
|
||||||
|
private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
|
||||||
const pattern = regexpHelper.escape(configKey)
|
const pattern = regexpHelper.escape(configKey)
|
||||||
await this.git.submoduleForeach(
|
await this.git.submoduleForeach(
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
|
||||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes includeIf entries that point to git-credentials-*.config files.
|
||||||
|
* @param configPath Optional path to a specific git config file to operate on
|
||||||
|
* @returns Array of unique credentials config file paths that were found and removed
|
||||||
|
*/
|
||||||
|
private async removeIncludeIfCredentials(
|
||||||
|
configPath?: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const credentialsPaths = new Set<string>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all includeIf.gitdir keys
|
||||||
|
const keys = await this.git.tryGetConfigKeys(
|
||||||
|
'^includeIf\\.gitdir:',
|
||||||
|
false, // globalConfig?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
// Get all values for this key
|
||||||
|
const values = await this.git.tryGetConfigValues(
|
||||||
|
key,
|
||||||
|
false, // globalConfig?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
|
if (values.length > 0) {
|
||||||
|
// Remove only values that match git-credentials-<uuid>.config pattern
|
||||||
|
for (const value of values) {
|
||||||
|
if (this.testCredentialsConfigPath(value)) {
|
||||||
|
credentialsPaths.add(value)
|
||||||
|
await this.git.tryConfigUnsetValue(key, value, false, configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore errors - this is cleanup code
|
||||||
|
if (configPath) {
|
||||||
|
core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
|
||||||
|
} else {
|
||||||
|
core.debug(`Error during includeIf cleanup: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(credentialsPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a path matches the git-credentials-*.config pattern.
|
||||||
|
* @param path The path to test
|
||||||
|
* @returns True if the path matches the credentials config pattern
|
||||||
|
*/
|
||||||
|
private testCredentialsConfigPath(path: string): boolean {
|
||||||
|
return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ export interface IGitCommandManager {
|
||||||
configKey: string,
|
configKey: string,
|
||||||
configValue: string,
|
configValue: string,
|
||||||
globalConfig?: boolean,
|
globalConfig?: boolean,
|
||||||
add?: boolean
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||||
fetch(
|
fetch(
|
||||||
|
|
@ -41,6 +42,7 @@ export interface IGitCommandManager {
|
||||||
}
|
}
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||||
|
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||||
getWorkingDirectory(): string
|
getWorkingDirectory(): string
|
||||||
init(): Promise<void>
|
init(): Promise<void>
|
||||||
isDetached(): Promise<boolean>
|
isDetached(): Promise<boolean>
|
||||||
|
|
@ -59,8 +61,24 @@ export interface IGitCommandManager {
|
||||||
tagExists(pattern: string): Promise<boolean>
|
tagExists(pattern: string): Promise<boolean>
|
||||||
tryClean(): Promise<boolean>
|
tryClean(): Promise<boolean>
|
||||||
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
|
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||||
|
tryConfigUnsetValue(
|
||||||
|
configKey: string,
|
||||||
|
configValue: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<boolean>
|
||||||
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||||||
tryGetFetchUrl(): Promise<string>
|
tryGetFetchUrl(): Promise<string>
|
||||||
|
tryGetConfigValues(
|
||||||
|
configKey: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<string[]>
|
||||||
|
tryGetConfigKeys(
|
||||||
|
pattern: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<string[]>
|
||||||
tryReset(): Promise<boolean>
|
tryReset(): Promise<boolean>
|
||||||
version(): Promise<GitVersion>
|
version(): Promise<GitVersion>
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +241,15 @@ class GitCommandManager {
|
||||||
configKey: string,
|
configKey: string,
|
||||||
configValue: string,
|
configValue: string,
|
||||||
globalConfig?: boolean,
|
globalConfig?: boolean,
|
||||||
add?: boolean
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
const args: string[] = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
if (add) {
|
if (add) {
|
||||||
args.push('--add')
|
args.push('--add')
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +347,21 @@ class GitCommandManager {
|
||||||
throw new Error('Unexpected output when retrieving default branch')
|
throw new Error('Unexpected output when retrieving default branch')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
|
||||||
|
// Get submodule config file paths.
|
||||||
|
// Use `--show-origin` to get the config file path for each submodule.
|
||||||
|
const output = await this.submoduleForeach(
|
||||||
|
`git config --local --show-origin --name-only --get-regexp remote.origin.url`,
|
||||||
|
recursive
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract config file paths from the output (lines starting with "file:").
|
||||||
|
const configPaths =
|
||||||
|
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||||
|
|
||||||
|
return configPaths
|
||||||
|
}
|
||||||
|
|
||||||
getWorkingDirectory(): string {
|
getWorkingDirectory(): string {
|
||||||
return this.workingDirectory
|
return this.workingDirectory
|
||||||
}
|
}
|
||||||
|
|
@ -455,6 +494,24 @@ class GitCommandManager {
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tryConfigUnsetValue(
|
||||||
|
configKey: string,
|
||||||
|
configValue: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const args = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
|
args.push('--unset', configKey, configValue)
|
||||||
|
|
||||||
|
const output = await this.execGit(args, true)
|
||||||
|
return output.exitCode === 0
|
||||||
|
}
|
||||||
|
|
||||||
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||||||
const output = await this.execGit(
|
const output = await this.execGit(
|
||||||
['config', '--local', 'gc.auto', '0'],
|
['config', '--local', 'gc.auto', '0'],
|
||||||
|
|
@ -481,6 +538,56 @@ class GitCommandManager {
|
||||||
return stdout
|
return stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tryGetConfigValues(
|
||||||
|
configKey: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const args = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
|
args.push('--get-all', configKey)
|
||||||
|
|
||||||
|
const output = await this.execGit(args, true)
|
||||||
|
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(value => value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryGetConfigKeys(
|
||||||
|
pattern: string,
|
||||||
|
globalConfig?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const args = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
|
args.push('--name-only', '--get-regexp', pattern)
|
||||||
|
|
||||||
|
const output = await this.execGit(args, true)
|
||||||
|
|
||||||
|
if (output.exitCode !== 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(key => key.trim())
|
||||||
|
}
|
||||||
|
|
||||||
async tryReset(): Promise<boolean> {
|
async tryReset(): Promise<boolean> {
|
||||||
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
|
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
|
|
|
||||||
|
|
@ -118,4 +118,9 @@ export interface IGitSourceSettings {
|
||||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||||
*/
|
*/
|
||||||
githubServerUrl: string | undefined
|
githubServerUrl: string | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the post action cleanup phase
|
||||||
|
*/
|
||||||
|
skipCleanup: boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ async function run(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanup(): Promise<void> {
|
async function cleanup(): Promise<void> {
|
||||||
|
const sourceSettings = await inputHelper.getInputs()
|
||||||
|
|
||||||
|
if (sourceSettings.skipCleanup) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
|
await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue