v6
This commit is contained in:
1 parent
b1d7621cc7
commit
feafc79e51
27 files changed
+1713
-626
No files matched your search
+227
-107
@@ -43,6 +43,7 @@ class GitAuthHelper {
|
||||
private sshKeyPath = ''
|
||||
private sshKnownHostsPath = ''
|
||||
private temporaryHomePath = ''
|
||||
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
|
||||
|
||||
constructor(
|
||||
gitCommandManager: IGitCommandManager,
|
||||
@@ -126,16 +127,21 @@ class GitAuthHelper {
|
||||
|
||||
async configureGlobalAuth(): Promise<void> {
|
||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
||||
await this.configureTempGlobalConfig()
|
||||
try {
|
||||
// Configure the token
|
||||
await this.configureToken(newGitConfigPath, true)
|
||||
await this.configureToken(true)
|
||||
|
||||
// Configure HTTPS instead of SSH
|
||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||
if (!this.settings.sshKey) {
|
||||
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) {
|
||||
@@ -150,24 +156,60 @@ class GitAuthHelper {
|
||||
|
||||
async configureSubmoduleAuth(): Promise<void> {
|
||||
// Remove possible previous HTTPS instead of SSH
|
||||
await this.removeGitConfig(this.insteadOfKey, true)
|
||||
await this.removeSubmoduleGitConfig(this.insteadOfKey)
|
||||
|
||||
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(
|
||||
// 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 '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
||||
// Get the credentials config file path in RUNNER_TEMP
|
||||
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||
|
||||
// Container credentials config path
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsConfigPath)
|
||||
)
|
||||
|
||||
// Get submodule config file paths.
|
||||
const configPaths = await this.git.getSubmoduleConfigPaths(
|
||||
this.settings.nestedSubmodules
|
||||
)
|
||||
|
||||
// Replace the placeholder
|
||||
const configPaths: string[] =
|
||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||
// Configure both host and container paths to support Docker container actions.
|
||||
for (const configPath of configPaths) {
|
||||
core.debug(`Replacing token placeholder in '${configPath}'`)
|
||||
await this.replaceTokenPlaceholder(configPath)
|
||||
// Submodule Git directory
|
||||
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) {
|
||||
@@ -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> {
|
||||
if (!this.settings.sshKey) {
|
||||
return
|
||||
@@ -272,57 +318,127 @@ class GitAuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private async configureToken(
|
||||
configPath?: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<void> {
|
||||
// Validate args
|
||||
assert.ok(
|
||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
||||
'Unexpected configureToken parameter combinations'
|
||||
)
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private async configureToken(globalConfig?: boolean): Promise<void> {
|
||||
// Get the credentials config file path in RUNNER_TEMP
|
||||
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||
|
||||
// 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
|
||||
// Write placeholder to the separate credentials config file using git config.
|
||||
// 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
|
||||
false, // globalConfig?
|
||||
false, // add?
|
||||
credentialsConfigPath
|
||||
)
|
||||
|
||||
// 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()
|
||||
// Replace the placeholder in the credentials config file
|
||||
let content = (await fs.promises.readFile(credentialsConfigPath)).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}`)
|
||||
throw new Error(
|
||||
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||
)
|
||||
}
|
||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||
content = content.replace(
|
||||
this.tokenPlaceholderConfigValue,
|
||||
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)
|
||||
|
||||
// Configure host includeIf for worktrees
|
||||
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
|
||||
await this.git.config(hostWorktreeIncludeKey, 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)
|
||||
|
||||
// Configure container includeIf for worktrees
|
||||
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
||||
await this.git.config(
|
||||
containerWorktreeIncludeKey,
|
||||
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> {
|
||||
// SSH key
|
||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||
if (keyPath) {
|
||||
try {
|
||||
core.info(`Removing SSH key '${keyPath}'`)
|
||||
await io.rmRF(keyPath)
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
@@ -335,88 +451,91 @@ class GitAuthHelper {
|
||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||
if (knownHostsPath) {
|
||||
try {
|
||||
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
|
||||
await io.rmRF(knownHostsPath)
|
||||
} catch {
|
||||
// Intentionally empty
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
|
||||
}
|
||||
}
|
||||
|
||||
// SSH command
|
||||
core.info('Removing SSH command configuration')
|
||||
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> {
|
||||
// Remove HTTP extra header from local git config and submodule configs
|
||||
// Remove HTTP extra header
|
||||
core.info('Removing HTTP extra header')
|
||||
await this.removeGitConfig(this.tokenConfigKey)
|
||||
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
|
||||
|
||||
//
|
||||
// Cleanup actions/checkout@v6 style credentials
|
||||
//
|
||||
const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
|
||||
if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
|
||||
core.debug(
|
||||
'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
|
||||
)
|
||||
return
|
||||
// Collect credentials config paths that need to be removed
|
||||
const credentialsPaths = new Set<string>()
|
||||
|
||||
// Remove includeIf entries that point to git-credentials-*.config files
|
||||
core.info('Removing includeIf entries pointing to credentials config files')
|
||||
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
|
||||
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||
const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
|
||||
for (const configPath of submoduleConfigPaths) {
|
||||
const submoduleCredentialsPaths =
|
||||
await this.removeIncludeIfCredentials(configPath)
|
||||
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
}
|
||||
|
||||
try {
|
||||
// Collect credentials config paths that need to be removed
|
||||
const credentialsPaths = new Set<string>()
|
||||
|
||||
// Remove includeIf entries that point to git-credentials-*.config files
|
||||
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
|
||||
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||
try {
|
||||
const submoduleConfigPaths =
|
||||
await this.git.getSubmoduleConfigPaths(true)
|
||||
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}'`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
core.debug(`Unable to get submodule config paths: ${err}`)
|
||||
} else {
|
||||
core.debug(
|
||||
`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
|
||||
)
|
||||
}
|
||||
|
||||
// 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']
|
||||
if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
|
||||
try {
|
||||
await io.rmRF(credentialsPath)
|
||||
} catch (err) {
|
||||
core.debug(
|
||||
`Failed to remove credentials config '${credentialsPath}': ${err}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
core.debug(`Failed to cleanup v6 style credentials: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
/**
|
||||
* 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)
|
||||
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}' || :"`,
|
||||
true
|
||||
)
|
||||
@@ -424,7 +543,6 @@ class GitAuthHelper {
|
||||
|
||||
/**
|
||||
* Removes includeIf entries that point to git-credentials-*.config files.
|
||||
* This handles cleanup of credentials configured by newer versions of the action.
|
||||
* @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
|
||||
*/
|
||||
@@ -460,16 +578,18 @@ class GitAuthHelper {
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors - this is cleanup code
|
||||
core.debug(
|
||||
`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
|
||||
)
|
||||
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 used by newer versions.
|
||||
* 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
|
||||
*/
|
||||
|
||||
+35
-12
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
): Promise<void>
|
||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||
fetch(
|
||||
@@ -36,14 +37,13 @@ export interface IGitCommandManager {
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||
getWorkingDirectory(): string
|
||||
init(): Promise<void>
|
||||
init(objectFormat?: string): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
@@ -240,9 +240,15 @@ class GitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
): 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) {
|
||||
args.push('--add')
|
||||
}
|
||||
@@ -273,14 +279,13 @@ class GitCommandManager {
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
// Always use --no-tags for explicit control over tag fetching
|
||||
// Tags are fetched explicitly via refspec when needed
|
||||
args.push('--no-tags')
|
||||
|
||||
args.push('--prune', '--no-recurse-submodules')
|
||||
if (options.showProgress) {
|
||||
@@ -359,8 +364,14 @@ class GitCommandManager {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.execGit(['init', this.workingDirectory])
|
||||
async init(objectFormat?: string): Promise<void> {
|
||||
const args = ['init']
|
||||
if (objectFormat === 'sha256') {
|
||||
args.push('--object-format=sha256')
|
||||
}
|
||||
args.push(this.workingDirectory)
|
||||
|
||||
await this.execGit(args)
|
||||
}
|
||||
|
||||
async isDetached(): Promise<boolean> {
|
||||
@@ -723,7 +734,19 @@ class GitCommandManager {
|
||||
}
|
||||
}
|
||||
// Set the user agent
|
||||
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||
let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||
|
||||
// Append orchestration ID if set
|
||||
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
if (orchId) {
|
||||
// Sanitize the orchestration ID to ensure it contains only valid characters
|
||||
// Valid characters: 0-9, a-z, _, -, .
|
||||
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
|
||||
if (sanitizedId) {
|
||||
gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||
}
|
||||
|
||||
@@ -109,8 +109,25 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Determining repository object format')
|
||||
const objectFormatResult =
|
||||
await githubApiHelper.tryGetRepositoryObjectFormat(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.githubServerUrl,
|
||||
settings.commit
|
||||
)
|
||||
const objectFormat = objectFormatResult.succeeded
|
||||
? objectFormatResult.format
|
||||
: ''
|
||||
if (objectFormat === 'sha256') {
|
||||
core.info('Detected SHA-256 repository object format')
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.init(objectFormat)
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
}
|
||||
@@ -159,7 +176,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
const fetchOptions: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
} = {}
|
||||
|
||||
@@ -182,12 +198,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
await git.fetch(refSpec, fetchOptions)
|
||||
|
||||
// Verify the ref now matches. For branches, the targeted fetch above brings
|
||||
// in the specific commit. For tags (fetched by ref), this will fail if
|
||||
// the tag was moved after the workflow was triggered.
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
throw new Error(
|
||||
`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
|
||||
`The ref may have been updated after the workflow was triggered.`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchOptions.fetchDepth = settings.fetchDepth
|
||||
fetchOptions.fetchTags = settings.fetchTags
|
||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
const refSpec = refHelper.getRefSpec(
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.fetchTags
|
||||
)
|
||||
await git.fetch(refSpec, fetchOptions)
|
||||
|
||||
// For tags, verify the ref still points to the expected commit.
|
||||
// Tags are fetched by ref (not commit), so if a tag was moved after the
|
||||
// workflow was triggered, we would silently check out the wrong commit.
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
throw new Error(
|
||||
`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
|
||||
`The ref may have been updated after the workflow was triggered.`
|
||||
)
|
||||
}
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ import {getServerApiUrl} from './url-helper'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
export interface RepositoryObjectFormatResult {
|
||||
format: string
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
export async function downloadRepository(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
@@ -122,6 +127,53 @@ export async function getDefaultBranch(
|
||||
})
|
||||
}
|
||||
|
||||
export async function tryGetRepositoryObjectFormat(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
baseUrl?: string,
|
||||
commit?: string
|
||||
): Promise<RepositoryObjectFormatResult> {
|
||||
const commitFormat = getObjectFormat(commit)
|
||||
if (commitFormat) {
|
||||
return {format: commitFormat, succeeded: true}
|
||||
}
|
||||
|
||||
try {
|
||||
const octokit = github.getOctokit(authToken, {
|
||||
baseUrl: getServerApiUrl(baseUrl)
|
||||
})
|
||||
const response = await octokit.request(
|
||||
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||
{owner, repo}
|
||||
)
|
||||
const hashAlgorithm = response.data.hash_algorithm
|
||||
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
||||
return {format: hashAlgorithm, succeeded: true}
|
||||
}
|
||||
|
||||
core.debug(
|
||||
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||
)
|
||||
return {format: '', succeeded: false}
|
||||
} catch (err) {
|
||||
core.debug(
|
||||
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
|
||||
)
|
||||
return {format: '', succeeded: false}
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectFormat(sha?: string): string {
|
||||
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
||||
return 'sha256'
|
||||
}
|
||||
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
||||
return 'sha1'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function downloadArchive(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
else if (asciiTrimmedRef.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
|
||||
result.commit = asciiTrimmedRef
|
||||
result.ref = ''
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v5',
|
||||
'actions/checkout@v6',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
||||
+39
-15
@@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function getRefSpec(ref: string, commit: string): string[] {
|
||||
export function getRefSpec(
|
||||
ref: string,
|
||||
commit: string,
|
||||
fetchTags?: boolean
|
||||
): string[] {
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
const result: string[] = []
|
||||
|
||||
// When fetchTags is true, always include the tags refspec
|
||||
if (fetchTags) {
|
||||
result.push(tagsRefSpec)
|
||||
}
|
||||
|
||||
// SHA
|
||||
if (commit) {
|
||||
// refs/heads
|
||||
if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
||||
result.push(`+${commit}:refs/remotes/origin/${branch}`)
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
||||
result.push(`+${commit}:refs/remotes/pull/${branch}`)
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
return [`+${commit}:${ref}`]
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
}
|
||||
// Otherwise no destination ref
|
||||
else {
|
||||
return [commit]
|
||||
result.push(commit)
|
||||
}
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else if (!upperRef.startsWith('REFS/')) {
|
||||
return [
|
||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
||||
]
|
||||
result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
|
||||
if (!fetchTags) {
|
||||
result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
|
||||
}
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
||||
result.push(`+${ref}:refs/remotes/origin/${branch}`)
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
||||
result.push(`+${ref}:refs/remotes/pull/${branch}`)
|
||||
}
|
||||
// refs/tags/
|
||||
else {
|
||||
return [`+${ref}:${ref}`]
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
}
|
||||
// Other refs
|
||||
else {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +190,10 @@ export async function testRef(
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
const tagName = ref.substring('refs/tags/'.length)
|
||||
// Use ^{commit} to dereference annotated tags to their underlying commit
|
||||
return (
|
||||
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
|
||||
(await git.tagExists(tagName)) &&
|
||||
commit === (await git.revParse(`${ref}^{commit}`))
|
||||
)
|
||||
}
|
||||
// Unexpected
|
||||
@@ -236,7 +258,9 @@ export async function checkCommitInfo(
|
||||
}
|
||||
|
||||
// Extract details from message
|
||||
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
|
||||
const match = commitInfo.match(
|
||||
/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/
|
||||
)
|
||||
if (!match) {
|
||||
core.debug('Unexpected message format')
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user