feat: signed commits (v7) (#3057)

* Add support for signed commits (#3055)

* formatting

* fix eslint and lint errors

* shift setting the base to before the push

* sign commits by default for testing

* add debug lines

* read to buffer not string and use non-legacy method to base64

* debug payload without contents

* disable linter for debug code

* fix filepath when using path input

* try to fix head repo

* remove commented code

* Try refactor of file changes

* add tests for building file changes

* add build file changes test for binary files

* refactor graphql code into github helper class

* build file changes even when there is no diff

* add function to get commit detail

* fix format

* build branch commits

* use source mode for deleted files

* try rest api route

* fix check for branch existence

* force push

* try fix base tree

* debug commit verification

* debug commit verification

* fix format and cleanup

* add executable mode file to test

* limit blob creation concurrency

* only build commits when feature enabled

* remove unused code

* update readme link

* update docs for commit signing

* fix capital letter

* update docs

* add throttling

* set default back to false

* output head sha and verified status

* log outputs

* fix head sha output

* default the operation output to none

* output retryafter for secondary rate limit

* use separate client for branch and pull operations

* add maintainer-can-modify input

* rename git-token to branch-token

* fix branch token input

* remove deprecated env output

* update docs

* fix doc

* update docs

* build branch commits when there is a diff with the base

* check verification status of head commit when not known

* fix verified output when no commit signing is being used

* draft always-true

* convert to draft on branch updates when there is a diff with base

* update docs with blob size limit

* catch errors during blob creation for debugging

* parse empty commits

* pass base commit to push signed commits

* use parent commit details in create commit

* use parent tree for base_tree

* multipart tree creation

* update docs

* update readme about the permissions of the default token

* fix edge case where changes are partially merged

* add updating documentation

* fix typo

* update major version

---------

Co-authored-by: Ravi <1299606+rustycl0ck@users.noreply.github.com>
This commit is contained in:
Peter Evans
2024-09-03 08:54:12 +01:00
committed by GitHub
parent 0c2a66fe4a
commit 4320041ed3
20 changed files with 32759 additions and 8594 deletions

View File

@@ -11,7 +11,7 @@ import * as utils from './utils'
export interface Inputs {
token: string
gitToken: string
branchToken: string
path: string
addPaths: string[]
commitMessage: string
@@ -23,6 +23,7 @@ export interface Inputs {
branchSuffix: string
base: string
pushToFork: string
signCommits: boolean
title: string
body: string
bodyPath: string
@@ -31,7 +32,11 @@ export interface Inputs {
reviewers: string[]
teamReviewers: string[]
milestone: number
draft: boolean
draft: {
value: boolean
always: boolean
}
maintainerCanModify: boolean
}
export async function createPullRequest(inputs: Inputs): Promise<void> {
@@ -45,8 +50,9 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
core.startGroup('Determining the base and head repositories')
const baseRemote = gitConfigHelper.getGitRemote()
// Init the GitHub client
const githubHelper = new GitHubHelper(baseRemote.hostname, inputs.token)
// Init the GitHub clients
const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.branchToken)
const ghPull = 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
@@ -57,11 +63,11 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
core.info(
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
)
const baseParentRepository = await githubHelper.getRepositoryParent(
const baseParentRepository = await ghBranch.getRepositoryParent(
baseRemote.repository
)
const branchParentRepository =
await githubHelper.getRepositoryParent(branchRepository)
await ghBranch.getRepositoryParent(branchRepository)
if (branchParentRepository == null) {
throw new Error(
`Repository '${branchRepository}' is not a fork. Unable to continue.`
@@ -91,7 +97,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication')
await gitConfigHelper.configureToken(inputs.gitToken)
await gitConfigHelper.configureToken(inputs.branchToken)
core.endGroup()
}
@@ -174,6 +180,11 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
)
core.endGroup()
// Action outputs
const outputs = new Map<string, string>()
outputs.set('pull-request-branch', inputs.branch)
outputs.set('pull-request-operation', 'none')
// Create or update the pull request branch
core.startGroup('Create or update the pull request branch')
const result = await createOrUpdateBranch(
@@ -185,6 +196,9 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
inputs.signoff,
inputs.addPaths
)
outputs.set('pull-request-head-sha', result.headSha)
// Set the base. It would have been '' if not specified as an input
inputs.base = result.base
core.endGroup()
if (['created', 'updated'].includes(result.action)) {
@@ -192,40 +206,55 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
core.startGroup(
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
)
await git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
])
if (inputs.signCommits) {
// Create signed commits via the GitHub API
const stashed = await git.stashPush(['--include-untracked'])
await git.checkout(inputs.branch)
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
result.branchCommits,
result.baseCommit,
repoPath,
branchRepository,
inputs.branch
)
outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha)
outputs.set(
'pull-request-commits-verified',
pushSignedCommitsResult.verified.toString()
)
await git.checkout('-')
if (stashed) {
await git.stashPop()
}
} else {
await git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
])
}
core.endGroup()
}
// Set the base. It would have been '' if not specified as an input
inputs.base = result.base
if (result.hasDiffWithBase) {
// Create or update the pull request
core.startGroup('Create or update the pull request')
const pull = await githubHelper.createOrUpdatePullRequest(
const pull = await ghPull.createOrUpdatePullRequest(
inputs,
baseRemote.repository,
branchRepository
)
core.endGroup()
// Set outputs
core.startGroup('Setting outputs')
core.setOutput('pull-request-number', pull.number)
core.setOutput('pull-request-url', pull.html_url)
outputs.set('pull-request-number', pull.number.toString())
outputs.set('pull-request-url', pull.html_url)
if (pull.created) {
core.setOutput('pull-request-operation', 'created')
outputs.set('pull-request-operation', 'created')
} else if (result.action == 'updated') {
core.setOutput('pull-request-operation', 'updated')
outputs.set('pull-request-operation', 'updated')
// The pull request was updated AND the branch was updated.
// Convert back to draft if 'draft: always-true' is set.
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
await ghPull.convertToDraft(pull.node_id)
}
}
core.setOutput('pull-request-head-sha', result.headSha)
core.setOutput('pull-request-branch', inputs.branch)
// Deprecated
core.exportVariable('PULL_REQUEST_NUMBER', pull.number)
core.endGroup()
} else {
// There is no longer a diff with the base
@@ -242,13 +271,45 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
branchRemoteName,
`refs/heads/${inputs.branch}`
])
// Set outputs
core.startGroup('Setting outputs')
core.setOutput('pull-request-operation', 'closed')
core.endGroup()
outputs.set('pull-request-operation', 'closed')
}
}
}
core.startGroup('Setting outputs')
// If the head commit is signed, get its verification status if we don't already know it.
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
if (
!outputs.has('pull-request-commits-verified') &&
result.branchCommits.length > 0 &&
result.branchCommits[result.branchCommits.length - 1].signed
) {
// Using the local head commit SHA because in this case commits have not been pushed via the API.
core.info(`Checking verification status of head commit ${result.headSha}`)
try {
const headCommit = await ghBranch.getCommit(
result.headSha,
branchRepository
)
outputs.set(
'pull-request-commits-verified',
headCommit.verified.toString()
)
} catch (error) {
core.warning('Failed to check verification status of head commit.')
core.debug(utils.getErrorMessage(error))
}
}
if (!outputs.has('pull-request-commits-verified')) {
outputs.set('pull-request-commits-verified', 'false')
}
// Set outputs
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`)
core.setOutput(key, value)
}
core.endGroup()
} catch (error) {
core.setFailed(utils.getErrorMessage(error))
} finally {