* 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>
This commit is contained in:
Peter Evans 2024-01-31 20:05:33 +09:00
parent bb809027fd
commit b1ddad2c99
24 changed files with 620 additions and 491 deletions

View file

@ -180,9 +180,12 @@ export async function createOrUpdateBranch(
if (branchRemoteName == 'fork') {
// If pushing to a fork we must fetch with 'unshallow' to avoid the following error on git push
// ! [remote rejected] HEAD -> tests/push-branch-to-fork (shallow update not allowed)
await git.fetch([`${workingBase}:${workingBase}`], baseRemote, [
'--force'
])
await git.fetch(
[`${workingBase}:${workingBase}`],
baseRemote,
['--force'],
true
)
} else {
// If the remote is 'origin' we can git reset
await git.checkout(workingBase)

View file

@ -6,11 +6,12 @@ import {
} from './create-or-update-branch'
import {GitHubHelper} from './github-helper'
import {GitCommandManager} from './git-command-manager'
import {GitAuthHelper} from './git-auth-helper'
import {GitConfigHelper} from './git-config-helper'
import * as utils from './utils'
export interface Inputs {
token: string
gitToken: string
path: string
addPaths: string[]
commitMessage: string
@ -34,45 +35,18 @@ export interface Inputs {
}
export async function createPullRequest(inputs: Inputs): Promise<void> {
let gitAuthHelper
let gitConfigHelper, git
try {
if (!inputs.token) {
throw new Error(`Input 'token' not supplied. Unable to continue.`)
}
if (inputs.bodyPath) {
if (!utils.fileExistsSync(inputs.bodyPath)) {
throw new Error(`File '${inputs.bodyPath}' does not exist.`)
}
// Update the body input with the contents of the file
inputs.body = utils.readFile(inputs.bodyPath)
}
// 65536 characters is the maximum allowed for the pull request body.
if (inputs.body.length > 65536) {
core.warning(
`Pull request body is too long. Truncating to 65536 characters.`
)
inputs.body = inputs.body.substring(0, 65536)
}
// Get the repository path
const repoPath = utils.getRepoPath(inputs.path)
// Create a git command manager
const git = await GitCommandManager.create(repoPath)
// Save and unset the extraheader auth config if it exists
core.startGroup('Prepare git configuration')
gitAuthHelper = new GitAuthHelper(git)
await gitAuthHelper.addSafeDirectory()
await gitAuthHelper.savePersistedAuth()
const repoPath = utils.getRepoPath(inputs.path)
git = await GitCommandManager.create(repoPath)
gitConfigHelper = await GitConfigHelper.create(git)
core.endGroup()
// Init the GitHub client
const githubHelper = new GitHubHelper(inputs.token)
core.startGroup('Determining the base and head repositories')
// Determine the base repository from git config
const remoteUrl = await git.tryGetRemoteUrl()
const baseRemote = utils.getRemoteDetail(remoteUrl)
const baseRemote = gitConfigHelper.getGitRemote()
// Init the GitHub client
const githubHelper = new GitHubHelper(baseRemote.hostname, inputs.token)
// Determine the head repository; the target for the pull request branch
const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'
const branchRepository = inputs.pushToFork
@ -83,11 +57,22 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
core.info(
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
)
const parentRepository =
const baseParentRepository = await githubHelper.getRepositoryParent(
baseRemote.repository
)
const branchParentRepository =
await githubHelper.getRepositoryParent(branchRepository)
if (parentRepository != baseRemote.repository) {
if (branchParentRepository == null) {
throw new Error(
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}'. Unable to continue.`
`Repository '${branchRepository}' is not a fork. Unable to continue.`
)
}
if (
branchParentRepository != baseRemote.repository &&
baseParentRepository != branchParentRepository
) {
throw new Error(
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`
)
}
// Add a remote for the fork
@ -106,7 +91,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication')
await gitAuthHelper.configureToken(inputs.token)
await gitConfigHelper.configureToken(inputs.gitToken)
core.endGroup()
}
@ -266,11 +251,11 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
} catch (error) {
core.setFailed(utils.getErrorMessage(error))
} finally {
// Remove auth and restore persisted auth config if it existed
core.startGroup('Restore git configuration')
await gitAuthHelper.removeAuth()
await gitAuthHelper.restorePersistedAuth()
await gitAuthHelper.removeSafeDirectory()
if (inputs.pushToFork) {
await git.exec(['remote', 'rm', 'fork'])
}
await gitConfigHelper.close()
core.endGroup()
}
}

View file

@ -105,7 +105,8 @@ export class GitCommandManager {
async fetch(
refSpec: string[],
remoteName?: string,
options?: string[]
options?: string[],
unshallow = false
): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === tagsRefSpec)) {
@ -113,7 +114,9 @@ export class GitCommandManager {
}
args.push('--progress', '--no-recurse-submodules')
if (
unshallow &&
utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))
) {
args.push('--unshallow')

View file

@ -5,22 +5,42 @@ import * as path from 'path'
import {URL} from 'url'
import * as utils from './utils'
export class GitAuthHelper {
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 extraheaderConfigKey: string
private remoteUrl = ''
private extraheaderConfigKey = ''
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
private persistedExtraheaderConfigValue = ''
constructor(git: GitCommandManager) {
private constructor(git: GitCommandManager) {
this.git = git
this.workingDirectory = this.git.getWorkingDirectory()
const serverUrl = this.getServerUrl()
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
}
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> {
@ -50,7 +70,57 @@ export class GitAuthHelper {
}
}
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()
}
@ -144,8 +214,4 @@ export class GitAuthHelper {
content = content.replace(find, replace)
await fs.promises.writeFile(this.gitConfigPath, content)
}
private getServerUrl(): URL {
return new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com')
}
}

View file

@ -20,12 +20,16 @@ interface Pull {
export class GitHubHelper {
private octokit: InstanceType<typeof Octokit>
constructor(token: string) {
constructor(githubServerHostname: string, token: string) {
const options: OctokitOptions = {}
if (token) {
options.auth = `${token}`
}
options.baseUrl = process.env['GITHUB_API_URL'] || 'https://api.github.com'
if (githubServerHostname !== 'github.com') {
options.baseUrl = `https://${githubServerHostname}/api/v3`
} else {
options.baseUrl = 'https://api.github.com'
}
this.octokit = new Octokit(options)
}
@ -101,14 +105,12 @@ export class GitHubHelper {
}
}
async getRepositoryParent(headRepository: string): Promise<string> {
async getRepositoryParent(headRepository: string): Promise<string | null> {
const {data: headRepo} = await this.octokit.rest.repos.get({
...this.parseRepository(headRepository)
})
if (!headRepo.parent) {
throw new Error(
`Repository '${headRepository}' is not a fork. Unable to continue.`
)
return null
}
return headRepo.parent.full_name
}

View file

@ -7,6 +7,7 @@ async function run(): Promise<void> {
try {
const inputs: Inputs = {
token: core.getInput('token'),
gitToken: core.getInput('git-token'),
path: core.getInput('path'),
addPaths: utils.getInputAsArray('add-paths'),
commitMessage: core.getInput('commit-message'),
@ -30,6 +31,27 @@ async function run(): Promise<void> {
}
core.debug(`Inputs: ${inspect(inputs)}`)
if (!inputs.token) {
throw new Error(`Input 'token' not supplied. Unable to continue.`)
}
if (!inputs.gitToken) {
inputs.gitToken = inputs.token
}
if (inputs.bodyPath) {
if (!utils.fileExistsSync(inputs.bodyPath)) {
throw new Error(`File '${inputs.bodyPath}' does not exist.`)
}
// Update the body input with the contents of the file
inputs.body = utils.readFile(inputs.bodyPath)
}
// 65536 characters is the maximum allowed for the pull request body.
if (inputs.body.length > 65536) {
core.warning(
`Pull request body is too long. Truncating to 65536 characters.`
)
inputs.body = inputs.body.substring(0, 65536)
}
await createPullRequest(inputs)
} catch (error) {
core.setFailed(utils.getErrorMessage(error))

View file

@ -41,53 +41,6 @@ export function getRepoPath(relativePath?: string): string {
return repoPath
}
interface RemoteDetail {
hostname: string
protocol: string
repository: string
}
export function getRemoteDetail(remoteUrl: string): RemoteDetail {
// Parse the protocol and github repository from a URL
// e.g. HTTPS, peter-evans/create-pull-request
const githubUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'
const githubServerMatch = githubUrl.match(/^https?:\/\/(.+)$/i)
if (!githubServerMatch) {
throw new Error('Could not parse GitHub Server name')
}
const hostname = githubServerMatch[1]
const httpsUrlPattern = new RegExp(
'^https?://.*@?' + hostname + '/(.+/.+?)(\\.git)?$',
'i'
)
const sshUrlPattern = new RegExp('^git@' + hostname + ':(.+/.+)\\.git$', 'i')
const httpsMatch = remoteUrl.match(httpsUrlPattern)
if (httpsMatch) {
return {
hostname,
protocol: 'HTTPS',
repository: httpsMatch[1]
}
}
const sshMatch = remoteUrl.match(sshUrlPattern)
if (sshMatch) {
return {
hostname,
protocol: 'SSH',
repository: sshMatch[1]
}
}
throw new Error(
`The format of '${remoteUrl}' is not a valid GitHub repository URL`
)
}
export function getRemoteUrl(
protocol: string,
hostname: string,