Compare commits

..

9 Commits

Author SHA1 Message Date
Peter Evans
740d9850a7 add maintainer-can-modify input 2024-08-16 15:44:43 +01:00
Peter Evans
e7f5ea9fd9 use separate client for branch and pull operations 2024-08-16 12:26:38 +00:00
Peter Evans
66ddf90dac output retryafter for secondary rate limit 2024-08-15 15:24:58 +00:00
Peter Evans
fd3e742ffd default the operation output to none 2024-08-15 13:37:41 +00:00
Peter Evans
eea4f44785 fix head sha output 2024-08-15 10:16:22 +00:00
Peter Evans
5a9be5875b log outputs 2024-08-14 21:55:31 +00:00
Peter Evans
3d665e5aea output head sha and verified status 2024-08-14 21:50:59 +00:00
Peter Evans
bb1f2b1327 set default back to false 2024-08-14 21:06:45 +00:00
Peter Evans
1da4bbe67c add throttling 2024-08-14 21:10:21 +01:00
7 changed files with 129 additions and 64 deletions

View File

@@ -75,6 +75,7 @@ All inputs are **optional**. If not set, sensible defaults will be used.
| `team-reviewers` | A comma or newline-separated list of GitHub teams to request a review from. Note that a `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), or equivalent [GitHub App permissions](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens), are required. | |
| `milestone` | The number of the milestone to associate this pull request with. | |
| `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). It is not possible to change draft status after creation except through the web interface. | `false` |
| `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` |
#### commit-message
@@ -116,9 +117,10 @@ The following outputs can be used by subsequent workflow steps.
- `pull-request-number` - The pull request number.
- `pull-request-url` - The URL of the pull request.
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated` or `closed`.
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated`, `closed` or `none`.
- `pull-request-head-sha` - The commit SHA of the pull request branch.
- `pull-request-branch` - The branch name of the pull request.
- `pull-request-commits-verified` - Whether GitHub considers the signature of the branch's commits to be verified; `true` or `false`.
Step outputs can be accessed as in the following example.
Note that in order to read the step outputs the action step must have an id.

View File

@@ -53,7 +53,7 @@ inputs:
The pull request will be created to merge the fork's branch into the parent's base.
sign-commits:
description: 'Sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using GitHub App tokens.'
default: true
default: false
title:
description: 'The title of the pull request.'
default: 'Changes by create-pull-request action'
@@ -77,6 +77,9 @@ inputs:
draft:
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
default: false
maintainer-can-modify:
description: 'Indicates whether maintainers can modify the pull request.'
default: true
outputs:
pull-request-number:
description: 'The pull request number'

80
dist/index.js vendored
View File

@@ -356,8 +356,9 @@ function createPullRequest(inputs) {
core.endGroup();
core.startGroup('Determining the base and head repositories');
const baseRemote = gitConfigHelper.getGitRemote();
// Init the GitHub client
const githubHelper = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.token);
// Init the GitHub clients
const ghBranch = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.gitToken);
const ghPull = new github_helper_1.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
@@ -366,8 +367,8 @@ function createPullRequest(inputs) {
if (inputs.pushToFork) {
// Check if the supplied fork is really a fork of the base
core.info(`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`);
const baseParentRepository = yield githubHelper.getRepositoryParent(baseRemote.repository);
const branchParentRepository = yield githubHelper.getRepositoryParent(branchRepository);
const baseParentRepository = yield ghBranch.getRepositoryParent(baseRemote.repository);
const branchParentRepository = yield ghBranch.getRepositoryParent(branchRepository);
if (branchParentRepository == null) {
throw new Error(`Repository '${branchRepository}' is not a fork. Unable to continue.`);
}
@@ -450,9 +451,15 @@ function createPullRequest(inputs) {
core.info(`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`);
core.info(`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`);
core.endGroup();
// Action outputs
const outputs = new Map();
outputs.set('pull-request-branch', inputs.branch);
outputs.set('pull-request-operation', 'none');
outputs.set('pull-request-commits-verified', 'false');
// Create or update the pull request branch
core.startGroup('Create or update the pull request branch');
const result = yield (0, create_or_update_branch_1.createOrUpdateBranch)(git, inputs.commitMessage, inputs.base, inputs.branch, branchRemoteName, inputs.signoff, inputs.addPaths, inputs.signCommits);
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();
@@ -463,7 +470,9 @@ function createPullRequest(inputs) {
// Create signed commits via the GitHub API
const stashed = yield git.stashPush(['--include-untracked']);
yield git.checkout(inputs.branch);
yield githubHelper.pushSignedCommits(result.branchCommits, result.baseSha, repoPath, branchRepository, inputs.branch);
const pushSignedCommitsResult = yield ghBranch.pushSignedCommits(result.branchCommits, result.baseSha, repoPath, branchRepository, inputs.branch);
outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha);
outputs.set('pull-request-commits-verified', pushSignedCommitsResult.verified.toString());
yield git.checkout('-');
if (stashed) {
yield git.stashPop();
@@ -479,22 +488,16 @@ function createPullRequest(inputs) {
core.endGroup();
}
if (result.hasDiffWithBase) {
// Create or update the pull request
core.startGroup('Create or update the pull request');
const pull = yield githubHelper.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);
const pull = yield ghPull.createOrUpdatePullRequest(inputs, baseRemote.repository, branchRepository);
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');
}
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();
@@ -512,13 +515,17 @@ function createPullRequest(inputs) {
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');
}
}
}
// Set outputs
core.startGroup('Setting outputs');
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`);
core.setOutput(key, value);
}
core.endGroup();
}
catch (error) {
core.setFailed(utils.getErrorMessage(error));
@@ -1162,7 +1169,9 @@ const core = __importStar(__nccwpck_require__(2186));
const octokit_client_1 = __nccwpck_require__(5040);
const p_limit_1 = __importDefault(__nccwpck_require__(3783));
const utils = __importStar(__nccwpck_require__(918));
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 = (0, p_limit_1.default)(8);
class GitHubHelper {
constructor(githubServerHostname, token) {
@@ -1193,7 +1202,7 @@ class GitHubHelper {
// Try to create the pull request
try {
core.info(`Attempting creation of pull request`);
const { data: pull } = yield this.octokit.rest.pulls.create(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { title: inputs.title, head: headBranch, head_repo: headRepository, base: inputs.base, body: inputs.body, draft: inputs.draft }));
const { data: pull } = yield this.octokit.rest.pulls.create(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { title: inputs.title, head: headBranch, head_repo: headRepository, base: inputs.base, body: inputs.body, draft: inputs.draft, maintainer_can_modify: inputs.maintainerCanModify }));
core.info(`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`);
return {
number: pull.number,
@@ -1202,9 +1211,15 @@ class GitHubHelper {
};
}
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;
}
@@ -1277,11 +1292,15 @@ class GitHubHelper {
}
pushSignedCommits(branchCommits, baseSha, repoPath, branchRepository, branch) {
return __awaiter(this, void 0, void 0, function* () {
let headSha = baseSha;
let headCommit = {
sha: baseSha,
verified: false
};
for (const commit of branchCommits) {
headSha = yield this.createCommit(commit, [headSha], repoPath, branchRepository);
headCommit = yield this.createCommit(commit, [headCommit.sha], repoPath, branchRepository);
}
yield this.createOrUpdateRef(branchRepository, branch, headSha);
yield this.createOrUpdateRef(branchRepository, branch, headCommit.sha);
return headCommit;
});
}
createCommit(commit, parents, repoPath, branchRepository) {
@@ -1312,7 +1331,10 @@ class GitHubHelper {
const { data: remoteCommit } = yield this.octokit.rest.git.createCommit(Object.assign(Object.assign({}, repository), { parents: parents, 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 remoteCommit.sha;
return {
sha: remoteCommit.sha,
verified: remoteCommit.verification.verified
};
});
}
createOrUpdateRef(branchRepository, branch, newHead) {
@@ -1405,7 +1427,8 @@ function run() {
reviewers: utils.getInputAsArray('reviewers'),
teamReviewers: utils.getInputAsArray('team-reviewers'),
milestone: Number(core.getInput('milestone')),
draft: core.getBooleanInput('draft')
draft: core.getBooleanInput('draft'),
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
};
core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`);
if (!inputs.token) {
@@ -1485,8 +1508,9 @@ exports.throttleOptions = {
return true;
}
},
onSecondaryRateLimit: (_, options) => {
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) => (url, opts) => {

View File

@@ -33,6 +33,7 @@ export interface Inputs {
teamReviewers: string[]
milestone: number
draft: boolean
maintainerCanModify: boolean
}
export async function createPullRequest(inputs: Inputs): Promise<void> {
@@ -46,8 +47,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.gitToken)
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
@@ -58,11 +60,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.`
@@ -175,6 +177,12 @@ 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')
outputs.set('pull-request-commits-verified', 'false')
// Create or update the pull request branch
core.startGroup('Create or update the pull request branch')
const result = await createOrUpdateBranch(
@@ -187,6 +195,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
inputs.addPaths,
inputs.signCommits
)
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()
@@ -200,13 +209,18 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
// Create signed commits via the GitHub API
const stashed = await git.stashPush(['--include-untracked'])
await git.checkout(inputs.branch)
await githubHelper.pushSignedCommits(
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
result.branchCommits,
result.baseSha,
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()
@@ -222,26 +236,19 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
}
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')
}
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()
@@ -260,13 +267,18 @@ 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')
}
}
}
// Set outputs
core.startGroup('Setting 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,8 +5,10 @@ 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)
@@ -21,6 +23,11 @@ interface Pull {
created: boolean
}
interface CommitResponse {
sha: string
verified: boolean
}
type TreeObject = {
path: string
mode: '100644' | '100755' | '040000' | '160000' | '120000'
@@ -71,7 +78,8 @@ export class GitHubHelper {
head_repo: headRepository,
base: inputs.base,
body: inputs.body,
draft: inputs.draft
draft: inputs.draft,
maintainer_can_modify: inputs.maintainerCanModify
})
core.info(
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
@@ -82,10 +90,17 @@ export class GitHubHelper {
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
}
@@ -203,17 +218,21 @@ export class GitHubHelper {
repoPath: string,
branchRepository: string,
branch: string
): Promise<void> {
let headSha = baseSha
): Promise<CommitResponse> {
let headCommit: CommitResponse = {
sha: baseSha,
verified: false
}
for (const commit of branchCommits) {
headSha = await this.createCommit(
headCommit = await this.createCommit(
commit,
[headSha],
[headCommit.sha],
repoPath,
branchRepository
)
}
await this.createOrUpdateRef(branchRepository, branch, headSha)
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
return headCommit
}
private async createCommit(
@@ -221,7 +240,7 @@ export class GitHubHelper {
parents: string[],
repoPath: string,
branchRepository: string
): Promise<string> {
): Promise<CommitResponse> {
const repository = this.parseRepository(branchRepository)
let treeSha = commit.tree
if (commit.changes.length > 0) {
@@ -270,7 +289,10 @@ export class GitHubHelper {
core.info(
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`
)
return remoteCommit.sha
return {
sha: remoteCommit.sha,
verified: remoteCommit.verification.verified
}
}
private async createOrUpdateRef(

View File

@@ -28,7 +28,8 @@ async function run(): Promise<void> {
reviewers: utils.getInputAsArray('reviewers'),
teamReviewers: utils.getInputAsArray('team-reviewers'),
milestone: Number(core.getInput('milestone')),
draft: core.getBooleanInput('draft')
draft: core.getBooleanInput('draft'),
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
}
core.debug(`Inputs: ${inspect(inputs)}`)

View File

@@ -25,10 +25,11 @@ export const throttleOptions = {
return true
}
},
onSecondaryRateLimit: (_, options) => {
onSecondaryRateLimit: (retryAfter, options) => {
core.warning(
`Hit secondary rate limit for request ${options.method} ${options.url}`
)
core.warning(`Requests may be retried after ${retryAfter} seconds.`)
}
}