b1ddad2c99
* feat: update author and committer input defaults * Update github-actions[bot] * Update author to new email format * feat: optional input for git ops token * feat: allow push-to-fork to push to sibling repos (#2414) Fixes #2412. * build: update dist * feat: update action runtime to node 20 (#2340) * feat: add truncate warning to pull request body * perf: unshallow only when necessary * fix: remove the remote for the fork on completion * feat: infer github server and api urls * test: integration test fixes * build: bump major version * docs: update to v6 --------- Co-authored-by: Teko <112829523+Teko012@users.noreply.github.com> Co-authored-by: Benjamin Gilbert <bgilbert@backtick.net>
217 lines
6.5 KiB
TypeScript
217 lines
6.5 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as fs from 'fs'
|
|
import {GitCommandManager} from './git-command-manager'
|
|
import * as path from 'path'
|
|
import {URL} from 'url'
|
|
import * as utils from './utils'
|
|
|
|
interface GitRemote {
|
|
hostname: string
|
|
protocol: string
|
|
repository: string
|
|
}
|
|
|
|
export class GitConfigHelper {
|
|
private git: GitCommandManager
|
|
private gitConfigPath = ''
|
|
private workingDirectory: string
|
|
private safeDirectoryConfigKey = 'safe.directory'
|
|
private safeDirectoryAdded = false
|
|
private remoteUrl = ''
|
|
private extraheaderConfigKey = ''
|
|
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
|
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
|
private persistedExtraheaderConfigValue = ''
|
|
|
|
private constructor(git: GitCommandManager) {
|
|
this.git = git
|
|
this.workingDirectory = this.git.getWorkingDirectory()
|
|
}
|
|
|
|
static async create(git: GitCommandManager): Promise<GitConfigHelper> {
|
|
const gitConfigHelper = new GitConfigHelper(git)
|
|
await gitConfigHelper.addSafeDirectory()
|
|
await gitConfigHelper.fetchRemoteDetail()
|
|
await gitConfigHelper.savePersistedAuth()
|
|
return gitConfigHelper
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
// Remove auth and restore persisted auth config if it existed
|
|
await this.removeAuth()
|
|
await this.restorePersistedAuth()
|
|
await this.removeSafeDirectory()
|
|
}
|
|
|
|
async addSafeDirectory(): Promise<void> {
|
|
const exists = await this.git.configExists(
|
|
this.safeDirectoryConfigKey,
|
|
this.workingDirectory,
|
|
true
|
|
)
|
|
if (!exists) {
|
|
await this.git.config(
|
|
this.safeDirectoryConfigKey,
|
|
this.workingDirectory,
|
|
true,
|
|
true
|
|
)
|
|
this.safeDirectoryAdded = true
|
|
}
|
|
}
|
|
|
|
async removeSafeDirectory(): Promise<void> {
|
|
if (this.safeDirectoryAdded) {
|
|
await this.git.tryConfigUnset(
|
|
this.safeDirectoryConfigKey,
|
|
this.workingDirectory,
|
|
true
|
|
)
|
|
}
|
|
}
|
|
|
|
async fetchRemoteDetail(): Promise<void> {
|
|
this.remoteUrl = await this.git.tryGetRemoteUrl()
|
|
}
|
|
|
|
getGitRemote(): GitRemote {
|
|
return GitConfigHelper.parseGitRemote(this.remoteUrl)
|
|
}
|
|
|
|
static parseGitRemote(remoteUrl: string): GitRemote {
|
|
const httpsUrlPattern = new RegExp(
|
|
'^(https?)://(?:.+@)?(.+?)/(.+/.+?)(\\.git)?$',
|
|
'i'
|
|
)
|
|
const httpsMatch = remoteUrl.match(httpsUrlPattern)
|
|
if (httpsMatch) {
|
|
return {
|
|
hostname: httpsMatch[2],
|
|
protocol: 'HTTPS',
|
|
repository: httpsMatch[3]
|
|
}
|
|
}
|
|
|
|
const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i')
|
|
const sshMatch = remoteUrl.match(sshUrlPattern)
|
|
if (sshMatch) {
|
|
return {
|
|
hostname: sshMatch[1],
|
|
protocol: 'SSH',
|
|
repository: sshMatch[2]
|
|
}
|
|
}
|
|
|
|
// Unauthenticated git protocol for integration tests only
|
|
const gitUrlPattern = new RegExp('^git://(.+?)/(.+/.+)\\.git$', 'i')
|
|
const gitMatch = remoteUrl.match(gitUrlPattern)
|
|
if (gitMatch) {
|
|
return {
|
|
hostname: gitMatch[1],
|
|
protocol: 'GIT',
|
|
repository: gitMatch[2]
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`The format of '${remoteUrl}' is not a valid GitHub repository URL`
|
|
)
|
|
}
|
|
|
|
async savePersistedAuth(): Promise<void> {
|
|
const serverUrl = new URL(`https://${this.getGitRemote().hostname}`)
|
|
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
|
// Save and unset persisted extraheader credential in git config if it exists
|
|
this.persistedExtraheaderConfigValue = await this.getAndUnset()
|
|
}
|
|
|
|
async restorePersistedAuth(): Promise<void> {
|
|
if (this.persistedExtraheaderConfigValue) {
|
|
try {
|
|
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
|
|
core.info('Persisted git credentials restored')
|
|
} catch (e) {
|
|
core.warning(utils.getErrorMessage(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
async configureToken(token: string): Promise<void> {
|
|
// Encode and configure the basic credential for HTTPS access
|
|
const basicCredential = Buffer.from(
|
|
`x-access-token:${token}`,
|
|
'utf8'
|
|
).toString('base64')
|
|
core.setSecret(basicCredential)
|
|
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`
|
|
await this.setExtraheaderConfig(extraheaderConfigValue)
|
|
}
|
|
|
|
async removeAuth(): Promise<void> {
|
|
await this.getAndUnset()
|
|
}
|
|
|
|
private async setExtraheaderConfig(
|
|
extraheaderConfigValue: string
|
|
): Promise<void> {
|
|
// 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
|
|
// See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274
|
|
await this.git.config(
|
|
this.extraheaderConfigKey,
|
|
this.extraheaderConfigPlaceholderValue
|
|
)
|
|
// Replace the placeholder
|
|
await this.gitConfigStringReplace(
|
|
this.extraheaderConfigPlaceholderValue,
|
|
extraheaderConfigValue
|
|
)
|
|
}
|
|
|
|
private async getAndUnset(): Promise<string> {
|
|
let configValue = ''
|
|
// Save and unset persisted extraheader credential in git config if it exists
|
|
if (
|
|
await this.git.configExists(
|
|
this.extraheaderConfigKey,
|
|
this.extraheaderConfigValueRegex
|
|
)
|
|
) {
|
|
configValue = await this.git.getConfigValue(
|
|
this.extraheaderConfigKey,
|
|
this.extraheaderConfigValueRegex
|
|
)
|
|
if (
|
|
await this.git.tryConfigUnset(
|
|
this.extraheaderConfigKey,
|
|
this.extraheaderConfigValueRegex
|
|
)
|
|
) {
|
|
core.info(`Unset config key '${this.extraheaderConfigKey}'`)
|
|
} else {
|
|
core.warning(
|
|
`Failed to unset config key '${this.extraheaderConfigKey}'`
|
|
)
|
|
}
|
|
}
|
|
return configValue
|
|
}
|
|
|
|
private async gitConfigStringReplace(
|
|
find: string,
|
|
replace: string
|
|
): Promise<void> {
|
|
if (this.gitConfigPath.length === 0) {
|
|
const gitDir = await this.git.getGitDirectory()
|
|
this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config')
|
|
}
|
|
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
|
|
const index = content.indexOf(find)
|
|
if (index < 0 || index != content.lastIndexOf(find)) {
|
|
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`)
|
|
}
|
|
content = content.replace(find, replace)
|
|
await fs.promises.writeFile(this.gitConfigPath, content)
|
|
}
|
|
}
|