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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 32759 additions and 8594 deletions

View file

@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {GitCommandManager} from './git-command-manager'
import {GitCommandManager, Commit} from './git-command-manager'
import {v4 as uuidv4} from 'uuid'
const CHERRYPICK_EMPTY =
@ -47,6 +47,27 @@ export async function tryFetch(
}
}
export async function buildBranchCommits(
git: GitCommandManager,
base: string,
branch: string
): Promise<Commit[]> {
const output = await git.exec(['log', '--format=%H', `${base}..${branch}`])
const shas = output.stdout
.split('\n')
.filter(x => x !== '')
.reverse()
const commits: Commit[] = []
for (const sha of shas) {
const commit = await git.getCommit(sha)
commits.push(commit)
for (const unparsedChange of commit.unparsedChanges) {
core.warning(`Skipping unexpected diff entry: ${unparsedChange}`)
}
}
return commits
}
// Return the number of commits that branch2 is ahead of branch1
async function commitsAhead(
git: GitCommandManager,
@ -103,6 +124,22 @@ async function isEven(
)
}
// Return true if the specified number of commits on branch1 and branch2 have a diff
async function commitsHaveDiff(
git: GitCommandManager,
branch1: string,
branch2: string,
depth: number
): Promise<boolean> {
const diff1 = (
await git.exec(['diff', '--stat', `${branch1}..${branch1}~${depth}`])
).stdout.trim()
const diff2 = (
await git.exec(['diff', '--stat', `${branch2}..${branch2}~${depth}`])
).stdout.trim()
return diff1 !== diff2
}
function splitLines(multilineString: string): string[] {
return multilineString
.split('\n')
@ -114,7 +151,9 @@ interface CreateOrUpdateBranchResult {
action: string
base: string
hasDiffWithBase: boolean
baseCommit: Commit
headSha: string
branchCommits: Commit[]
}
export async function createOrUpdateBranch(
@ -139,14 +178,6 @@ export async function createOrUpdateBranch(
base = base ? base : workingBase
const baseRemote = 'origin'
// Set the default return values
const result: CreateOrUpdateBranchResult = {
action: 'none',
base: base,
hasDiffWithBase: false,
headSha: ''
}
// Save the working base changes to a temporary branch
const tempBranch = uuidv4()
await git.checkout(tempBranch, 'HEAD')
@ -226,6 +257,9 @@ export async function createOrUpdateBranch(
? tempBranchCommitsAhead + FETCH_DEPTH_MARGIN
: FETCH_DEPTH_MARGIN
let action = 'none'
let hasDiffWithBase = false
// Try to fetch the pull request branch
if (!(await tryFetch(git, branchRemoteName, branch, fetchDepth))) {
// The pull request branch does not exist
@ -233,9 +267,9 @@ export async function createOrUpdateBranch(
// Create the pull request branch
await git.checkout(branch, tempBranch)
// Check if the pull request branch is ahead of the base
result.hasDiffWithBase = await isAhead(git, base, branch)
if (result.hasDiffWithBase) {
result.action = 'created'
hasDiffWithBase = await isAhead(git, base, branch)
if (hasDiffWithBase) {
action = 'created'
core.info(`Created branch '${branch}'`)
} else {
core.info(
@ -252,20 +286,26 @@ export async function createOrUpdateBranch(
// Reset the branch if one of the following conditions is true.
// - If the branch differs from the recreated temp branch.
// - If the number of commits ahead of the base branch differs between the branch and
// temp branch. This catches a case where the base branch has been force pushed to
// a new commit.
// - If the recreated temp branch is not ahead of the base. This means there will be
// no pull request diff after the branch is reset. This will reset any undeleted
// branches after merging. In particular, it catches a case where the branch was
// squash merged but not deleted. We need to reset to make sure it doesn't appear
// to have a diff with the base due to different commits for the same changes.
// - If the number of commits ahead of the base branch differs between the branch and
// temp branch. This catches a case where the base branch has been force pushed to
// a new commit.
// - If the diff of the commits ahead of the base branch differs between the branch and
// temp branch. This catches a case where changes have been partially merged to the
// base. The overall diff is the same, but the branch needs to be rebased to show
// the correct diff.
//
// For changes on base this reset is equivalent to a rebase of the pull request branch.
const branchCommitsAhead = await commitsAhead(git, base, branch)
if (
(await git.hasDiff([`${branch}..${tempBranch}`])) ||
branchCommitsAhead != tempBranchCommitsAhead ||
!(tempBranchCommitsAhead > 0) // !isAhead
!(tempBranchCommitsAhead > 0) || // !isAhead
(await commitsHaveDiff(git, branch, tempBranch, tempBranchCommitsAhead))
) {
core.info(`Resetting '${branch}'`)
// Alternatively, git switch -C branch tempBranch
@ -276,21 +316,29 @@ export async function createOrUpdateBranch(
// If the branch was reset or updated it will be ahead
// It may be behind if a reset now results in no diff with the base
if (!(await isEven(git, `${branchRemoteName}/${branch}`, branch))) {
result.action = 'updated'
action = 'updated'
core.info(`Updated branch '${branch}'`)
} else {
result.action = 'not-updated'
action = 'not-updated'
core.info(
`Branch '${branch}' is even with its remote and will not be updated`
)
}
// Check if the pull request branch is ahead of the base
result.hasDiffWithBase = await isAhead(git, base, branch)
hasDiffWithBase = await isAhead(git, base, branch)
}
// Get the pull request branch SHA
result.headSha = await git.revParse('HEAD')
// Get the base and head SHAs
const baseSha = await git.revParse(base)
const baseCommit = await git.getCommit(baseSha)
const headSha = await git.revParse(branch)
let branchCommits: Commit[] = []
if (hasDiffWithBase) {
// Build the branch commits
branchCommits = await buildBranchCommits(git, base, branch)
}
// Delete the temporary branch
await git.exec(['branch', '--delete', '--force', tempBranch])
@ -303,5 +351,12 @@ export async function createOrUpdateBranch(
await git.stashPop()
}
return result
return {
action: action,
base: base,
hasDiffWithBase: hasDiffWithBase,
baseCommit: baseCommit,
headSha: headSha,
branchCommits: branchCommits
}
}

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 {

View file

@ -5,6 +5,21 @@ import * as path from 'path'
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
export type Commit = {
sha: string
tree: string
parents: string[]
signed: boolean
subject: string
body: string
changes: {
mode: string
status: 'A' | 'M' | 'D'
path: string
}[]
unparsedChanges: string[]
}
export class GitCommandManager {
private gitPath: string
private workingDirectory: string
@ -138,6 +153,45 @@ export class GitCommandManager {
await this.exec(args)
}
async getCommit(ref: string): Promise<Commit> {
const endOfBody = '###EOB###'
const output = await this.exec([
'show',
'--raw',
'--cc',
`--format=%H%n%T%n%P%n%G?%n%s%n%b%n${endOfBody}`,
ref
])
const lines = output.stdout.split('\n')
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
const detailLines = lines.slice(0, endOfBodyIndex)
const unparsedChanges: string[] = []
return <Commit>{
sha: detailLines[0],
tree: detailLines[1],
parents: detailLines[2].split(' '),
signed: detailLines[3] !== 'N',
subject: detailLines[4],
body: detailLines.slice(5, endOfBodyIndex).join('\n'),
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
const change = line.match(
/^:(\d{6}) (\d{6}) \w{7} \w{7} ([AMD])\s+(.*)$/
)
if (change) {
return {
mode: change[3] === 'D' ? change[1] : change[2],
status: change[3],
path: change[4]
}
} else {
unparsedChanges.push(line)
}
}),
unparsedChanges: unparsedChanges
}
}
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
const output = await this.exec([
'config',

View file

@ -1,10 +1,16 @@
import * as core from '@actions/core'
import {Inputs} from './create-pull-request'
import {Octokit, OctokitOptions} from './octokit-client'
import {Commit} from './git-command-manager'
import {Octokit, OctokitOptions, throttleOptions} from './octokit-client'
import pLimit from 'p-limit'
import * as utils from './utils'
const ERROR_PR_ALREADY_EXISTS = 'A pull request already exists for'
const ERROR_PR_REVIEW_TOKEN_SCOPE =
'Validation Failed: "Could not resolve to a node with the global id of'
const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission`
const blobCreationLimit = pLimit(8)
interface Repository {
owner: string
@ -14,9 +20,24 @@ interface Repository {
interface Pull {
number: number
html_url: string
node_id: string
draft?: boolean
created: boolean
}
interface CommitResponse {
sha: string
tree: string
verified: boolean
}
type TreeObject = {
path: string
mode: '100644' | '100755' | '040000' | '160000' | '120000'
sha: string | null
type: 'blob'
}
export class GitHubHelper {
private octokit: InstanceType<typeof Octokit>
@ -30,6 +51,7 @@ export class GitHubHelper {
} else {
options.baseUrl = 'https://api.github.com'
}
options.throttle = throttleOptions
this.octokit = new Octokit(options)
}
@ -59,7 +81,8 @@ export class GitHubHelper {
head_repo: headRepository,
base: inputs.base,
body: inputs.body,
draft: inputs.draft
draft: inputs.draft.value,
maintainer_can_modify: inputs.maintainerCanModify
})
core.info(
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
@ -67,13 +90,22 @@ export class GitHubHelper {
return {
number: pull.number,
html_url: pull.html_url,
node_id: pull.node_id,
draft: pull.draft,
created: true
}
} catch (e) {
if (
utils.getErrorMessage(e).includes(`A pull request already exists for`)
) {
const errorMessage = utils.getErrorMessage(e)
if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) {
core.info(`A pull request already exists for ${headBranch}`)
} else if (errorMessage.includes(ERROR_PR_FORK_COLLAB)) {
core.warning(
'An attempt was made to create a pull request using a token that does not have write access to the head branch.'
)
core.warning(
`For this case, set input 'maintainer-can-modify' to 'false' to allow pull request creation.`
)
throw e
} else {
throw e
}
@ -100,6 +132,8 @@ export class GitHubHelper {
return {
number: pull.number,
html_url: pull.html_url,
node_id: pull.node_id,
draft: pull.draft,
created: false
}
}
@ -184,4 +218,175 @@ export class GitHubHelper {
return pull
}
async pushSignedCommits(
branchCommits: Commit[],
baseCommit: Commit,
repoPath: string,
branchRepository: string,
branch: string
): Promise<CommitResponse> {
let headCommit: CommitResponse = {
sha: baseCommit.sha,
tree: baseCommit.tree,
verified: false
}
for (const commit of branchCommits) {
headCommit = await this.createCommit(
commit,
headCommit,
repoPath,
branchRepository
)
}
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
return headCommit
}
private async createCommit(
commit: Commit,
parentCommit: CommitResponse,
repoPath: string,
branchRepository: string
): Promise<CommitResponse> {
const repository = this.parseRepository(branchRepository)
// In the case of an empty commit, the tree references the parent's tree
let treeSha = parentCommit.tree
if (commit.changes.length > 0) {
core.info(`Creating tree objects for local commit ${commit.sha}`)
const treeObjects = await Promise.all(
commit.changes.map(async ({path, mode, status}) => {
let sha: string | null = null
if (status === 'A' || status === 'M') {
try {
const {data: blob} = await blobCreationLimit(() =>
this.octokit.rest.git.createBlob({
...repository,
content: utils.readFileBase64([repoPath, path]),
encoding: 'base64'
})
)
sha = blob.sha
} catch (error) {
core.error(
`Error creating blob for file '${path}': ${utils.getErrorMessage(error)}`
)
throw error
}
}
core.info(`Created blob for file '${path}'`)
return <TreeObject>{
path,
mode,
sha,
type: 'blob'
}
})
)
const chunkSize = 100
const chunkedTreeObjects: TreeObject[][] = Array.from(
{length: Math.ceil(treeObjects.length / chunkSize)},
(_, i) => treeObjects.slice(i * chunkSize, i * chunkSize + chunkSize)
)
core.info(`Creating tree for local commit ${commit.sha}`)
for (let i = 0; i < chunkedTreeObjects.length; i++) {
const {data: tree} = await this.octokit.rest.git.createTree({
...repository,
base_tree: treeSha,
tree: chunkedTreeObjects[i]
})
treeSha = tree.sha
if (chunkedTreeObjects.length > 1) {
core.info(
`Created tree ${treeSha} of multipart tree (${i + 1} of ${chunkedTreeObjects.length})`
)
}
}
core.info(`Created tree ${treeSha} for local commit ${commit.sha}`)
}
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
...repository,
parents: [parentCommit.sha],
tree: treeSha,
message: `${commit.subject}\n\n${commit.body}`
})
core.info(
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
)
core.info(
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`
)
return {
sha: remoteCommit.sha,
tree: remoteCommit.tree.sha,
verified: remoteCommit.verification.verified
}
}
async getCommit(
sha: string,
branchRepository: string
): Promise<CommitResponse> {
const repository = this.parseRepository(branchRepository)
const {data: remoteCommit} = await this.octokit.rest.git.getCommit({
...repository,
commit_sha: sha
})
return {
sha: remoteCommit.sha,
tree: remoteCommit.tree.sha,
verified: remoteCommit.verification.verified
}
}
private async createOrUpdateRef(
branchRepository: string,
branch: string,
newHead: string
) {
const repository = this.parseRepository(branchRepository)
const branchExists = await this.octokit.rest.repos
.getBranch({
...repository,
branch: branch
})
.then(
() => true,
() => false
)
if (branchExists) {
core.info(`Branch ${branch} exists; Updating ref`)
await this.octokit.rest.git.updateRef({
...repository,
sha: newHead,
ref: `heads/${branch}`,
force: true
})
} else {
core.info(`Branch ${branch} does not exist; Creating ref`)
await this.octokit.rest.git.createRef({
...repository,
sha: newHead,
ref: `refs/heads/${branch}`
})
}
}
async convertToDraft(id: string): Promise<void> {
core.info(`Converting pull request to draft`)
await this.octokit.graphql({
query: `mutation($pullRequestId: ID!) {
convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {
pullRequest {
isDraft
}
}
}`,
pullRequestId: id
})
}
}

View file

@ -3,11 +3,19 @@ import {Inputs, createPullRequest} from './create-pull-request'
import {inspect} from 'util'
import * as utils from './utils'
function getDraftInput(): {value: boolean; always: boolean} {
if (core.getInput('draft') === 'always-true') {
return {value: true, always: true}
} else {
return {value: core.getBooleanInput('draft'), always: false}
}
}
async function run(): Promise<void> {
try {
const inputs: Inputs = {
token: core.getInput('token'),
gitToken: core.getInput('git-token'),
branchToken: core.getInput('branch-token'),
path: core.getInput('path'),
addPaths: utils.getInputAsArray('add-paths'),
commitMessage: core.getInput('commit-message'),
@ -19,6 +27,7 @@ async function run(): Promise<void> {
branchSuffix: core.getInput('branch-suffix'),
base: core.getInput('base'),
pushToFork: core.getInput('push-to-fork'),
signCommits: core.getBooleanInput('sign-commits'),
title: core.getInput('title'),
body: core.getInput('body'),
bodyPath: core.getInput('body-path'),
@ -27,15 +36,16 @@ async function run(): Promise<void> {
reviewers: utils.getInputAsArray('reviewers'),
teamReviewers: utils.getInputAsArray('team-reviewers'),
milestone: Number(core.getInput('milestone')),
draft: core.getBooleanInput('draft')
draft: getDraftInput(),
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
}
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.branchToken) {
inputs.branchToken = inputs.token
}
if (inputs.bodyPath) {
if (!utils.fileExistsSync(inputs.bodyPath)) {

View file

@ -1,17 +1,38 @@
import {Octokit as Core} from '@octokit/core'
import * as core from '@actions/core'
import {Octokit as OctokitCore} from '@octokit/core'
import {paginateRest} from '@octokit/plugin-paginate-rest'
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
import {throttling} from '@octokit/plugin-throttling'
import {getProxyForUrl} from 'proxy-from-env'
import {ProxyAgent, fetch as undiciFetch} from 'undici'
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
// eslint-disable-next-line import/no-unresolved
export {OctokitOptions} from '@octokit/core/dist-types/types'
export const Octokit = Core.plugin(
export const Octokit = OctokitCore.plugin(
paginateRest,
restEndpointMethods,
throttling,
autoProxyAgent
)
export const throttleOptions = {
onRateLimit: (retryAfter, options, _, retryCount) => {
core.debug(`Hit rate limit for request ${options.method} ${options.url}`)
// Retries twice for a total of three attempts
if (retryCount < 2) {
core.debug(`Retrying after ${retryAfter} seconds!`)
return true
}
},
onSecondaryRateLimit: (retryAfter, options) => {
core.warning(
`Hit secondary rate limit for request ${options.method} ${options.url}`
)
core.warning(`Requests may be retried after ${retryAfter} seconds.`)
}
}
const proxyFetch =
(proxyUrl: string): typeof undiciFetch =>
(url, opts) => {
@ -24,7 +45,7 @@ const proxyFetch =
}
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
function autoProxyAgent(octokit: Core) {
function autoProxyAgent(octokit: OctokitCore) {
octokit.hook.before('request', options => {
const proxy = getProxyForUrl(options.baseUrl)
if (proxy) {

View file

@ -126,6 +126,10 @@ export function readFile(path: string): string {
return fs.readFileSync(path, 'utf-8')
}
export function readFileBase64(pathParts: string[]): string {
return fs.readFileSync(path.resolve(...pathParts)).toString('base64')
}
/* eslint-disable @typescript-eslint/no-explicit-any */
function hasErrorCode(error: any): error is {code: string} {
return typeof (error && error.code) === 'string'