Add support for signed commits (#3055)
This commit is contained in:
parent
93bc7fd9cd
commit
70815fee7e
8 changed files with 27416 additions and 2208 deletions
|
@ -74,6 +74,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. | |
|
| `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. | |
|
| `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` |
|
| `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` |
|
||||||
|
| `sign-commit` | Sign the commit as bot [refer: [Signature verification for bots](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#signature-verification-for-bots)]. This can be useful if your repo or org has enforced commit-signing. | `false` |
|
||||||
|
|
||||||
#### commit-message
|
#### commit-message
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,9 @@ inputs:
|
||||||
draft:
|
draft:
|
||||||
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
|
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
|
||||||
default: false
|
default: false
|
||||||
|
sign-commit:
|
||||||
|
description: 'Sign the commit as github-actions bot (and as custom app if a different github-token is provided)'
|
||||||
|
default: false
|
||||||
outputs:
|
outputs:
|
||||||
pull-request-number:
|
pull-request-number:
|
||||||
description: 'The pull request number'
|
description: 'The pull request number'
|
||||||
|
|
26079
dist/index.js
vendored
26079
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3336
package-lock.json
generated
3336
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -32,6 +32,8 @@
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@octokit/core": "^4.2.4",
|
"@octokit/core": "^4.2.4",
|
||||||
|
"@octokit/graphql": "^8.1.1",
|
||||||
|
"@octokit/graphql-schema": "^15.25.0",
|
||||||
"@octokit/plugin-paginate-rest": "^5.0.1",
|
"@octokit/plugin-paginate-rest": "^5.0.1",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^6.8.1",
|
"@octokit/plugin-rest-endpoint-methods": "^6.8.1",
|
||||||
"proxy-from-env": "^1.1.0",
|
"proxy-from-env": "^1.1.0",
|
||||||
|
@ -55,6 +57,6 @@
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"ts-jest": "^29.2.3",
|
"ts-jest": "^29.2.3",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import { graphql } from '@octokit/graphql'
|
||||||
|
import type {
|
||||||
|
Repository,
|
||||||
|
Ref,
|
||||||
|
Commit,
|
||||||
|
FileChanges
|
||||||
|
} from '@octokit/graphql-schema'
|
||||||
import {
|
import {
|
||||||
createOrUpdateBranch,
|
createOrUpdateBranch,
|
||||||
getWorkingBaseAndType,
|
getWorkingBaseAndType,
|
||||||
|
@ -32,6 +40,7 @@ export interface Inputs {
|
||||||
teamReviewers: string[]
|
teamReviewers: string[]
|
||||||
milestone: number
|
milestone: number
|
||||||
draft: boolean
|
draft: boolean
|
||||||
|
signCommit: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
|
@ -192,11 +201,180 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
core.startGroup(
|
core.startGroup(
|
||||||
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
||||||
)
|
)
|
||||||
await git.push([
|
if (inputs.signCommit) {
|
||||||
'--force-with-lease',
|
core.info(`Use API to push a signed commit`)
|
||||||
branchRemoteName,
|
const graphqlWithAuth = graphql.defaults({
|
||||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
headers: {
|
||||||
])
|
authorization: 'token ' + inputs.token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let repoOwner = process.env.GITHUB_REPOSITORY!.split("/")[0]
|
||||||
|
if (inputs.pushToFork) {
|
||||||
|
const forkName = await githubHelper.getRepositoryParent(baseRemote.repository)
|
||||||
|
if (!forkName) { repoOwner = forkName! }
|
||||||
|
}
|
||||||
|
const repoName = process.env.GITHUB_REPOSITORY!.split("/")[1]
|
||||||
|
|
||||||
|
core.debug(`repoOwner: '${repoOwner}', repoName: '${repoName}'`)
|
||||||
|
const refQuery = `
|
||||||
|
query GetRefId($repoName: String!, $repoOwner: String!, $branchName: String!) {
|
||||||
|
repository(owner: $repoOwner, name: $repoName){
|
||||||
|
id
|
||||||
|
ref(qualifiedName: $branchName){
|
||||||
|
id
|
||||||
|
name
|
||||||
|
prefix
|
||||||
|
target{
|
||||||
|
id
|
||||||
|
oid
|
||||||
|
commitUrl
|
||||||
|
commitResourcePath
|
||||||
|
abbreviatedOid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
let branchRef = await graphqlWithAuth<{repository: Repository}>(
|
||||||
|
refQuery,
|
||||||
|
{
|
||||||
|
repoOwner: repoOwner,
|
||||||
|
repoName: repoName,
|
||||||
|
branchName: inputs.branch
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug( `Fetched information for branch '${inputs.branch}' - '${JSON.stringify(branchRef)}'`)
|
||||||
|
|
||||||
|
// if the branch does not exist, then first we need to create the branch from base
|
||||||
|
if (branchRef.repository.ref == null) {
|
||||||
|
core.debug( `Branch does not exist - '${inputs.branch}'`)
|
||||||
|
branchRef = await graphqlWithAuth<{repository: Repository}>(
|
||||||
|
refQuery,
|
||||||
|
{
|
||||||
|
repoOwner: repoOwner,
|
||||||
|
repoName: repoName,
|
||||||
|
branchName: inputs.base
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug( `Fetched information for base branch '${inputs.base}' - '${JSON.stringify(branchRef)}'`)
|
||||||
|
|
||||||
|
core.info( `Creating new branch '${inputs.branch}' from '${inputs.base}', with ref '${JSON.stringify(branchRef.repository.ref!.target!.oid)}'`)
|
||||||
|
if (branchRef.repository.ref != null) {
|
||||||
|
core.debug( `Send request for creating new branch`)
|
||||||
|
const newBranchMutation = `
|
||||||
|
mutation CreateNewBranch($branchName: String!, $oid: GitObjectID!, $repoId: ID!) {
|
||||||
|
createRef(input: {
|
||||||
|
name: $branchName,
|
||||||
|
oid: $oid,
|
||||||
|
repositoryId: $repoId
|
||||||
|
}) {
|
||||||
|
ref {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
let newBranch = await graphqlWithAuth<{createRef: {ref: Ref}}>(
|
||||||
|
newBranchMutation,
|
||||||
|
{
|
||||||
|
repoId: branchRef.repository.id,
|
||||||
|
oid: branchRef.repository.ref.target!.oid,
|
||||||
|
branchName: 'refs/heads/' + inputs.branch
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug(`Created new branch '${inputs.branch}': '${JSON.stringify(newBranch.createRef.ref)}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.info( `Hash ref of branch '${inputs.branch}' is '${JSON.stringify(branchRef.repository.ref!.target!.oid)}'`)
|
||||||
|
|
||||||
|
// switch to input-branch for reading updated file contents
|
||||||
|
await git.checkout(inputs.branch)
|
||||||
|
|
||||||
|
let changedFiles = await git.getChangedFiles(branchRef.repository.ref!.target!.oid, ['--diff-filter=M'])
|
||||||
|
let deletedFiles = await git.getChangedFiles(branchRef.repository.ref!.target!.oid, ['--diff-filter=D'])
|
||||||
|
let fileChanges = <FileChanges>{additions: [], deletions: []}
|
||||||
|
|
||||||
|
core.debug(`Changed files: '${JSON.stringify(changedFiles)}'`)
|
||||||
|
core.debug(`Deleted files: '${JSON.stringify(deletedFiles)}'`)
|
||||||
|
|
||||||
|
for (var file of changedFiles) {
|
||||||
|
fileChanges.additions!.push({
|
||||||
|
path: file,
|
||||||
|
contents: btoa(fs.readFileSync(file, 'utf8')),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var file of deletedFiles) {
|
||||||
|
fileChanges.deletions!.push({
|
||||||
|
path: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushCommitMutation = `
|
||||||
|
mutation PushCommit(
|
||||||
|
$repoNameWithOwner: String!,
|
||||||
|
$branchName: String!,
|
||||||
|
$headOid: GitObjectID!,
|
||||||
|
$commitMessage: String!,
|
||||||
|
$fileChanges: FileChanges
|
||||||
|
) {
|
||||||
|
createCommitOnBranch(input: {
|
||||||
|
branch: {
|
||||||
|
repositoryNameWithOwner: $repoNameWithOwner,
|
||||||
|
branchName: $branchName,
|
||||||
|
}
|
||||||
|
fileChanges: $fileChanges
|
||||||
|
message: {
|
||||||
|
headline: $commitMessage
|
||||||
|
}
|
||||||
|
expectedHeadOid: $headOid
|
||||||
|
}){
|
||||||
|
clientMutationId
|
||||||
|
ref{
|
||||||
|
id
|
||||||
|
name
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
commit{
|
||||||
|
id
|
||||||
|
abbreviatedOid
|
||||||
|
oid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const pushCommitVars = {
|
||||||
|
branchName: inputs.branch,
|
||||||
|
repoNameWithOwner: repoOwner + '/' + repoName,
|
||||||
|
headOid: branchRef.repository.ref!.target!.oid,
|
||||||
|
commitMessage: inputs.commitMessage,
|
||||||
|
fileChanges: fileChanges,
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Push commit with payload: '${JSON.stringify(pushCommitVars)}'`)
|
||||||
|
|
||||||
|
const commit = await graphqlWithAuth<{createCommitOnBranch: {ref: Ref, commit: Commit} }>(
|
||||||
|
pushCommitMutation,
|
||||||
|
pushCommitVars,
|
||||||
|
);
|
||||||
|
|
||||||
|
core.debug( `Pushed commit - '${JSON.stringify(commit)}'`)
|
||||||
|
core.info( `Pushed commit with hash - '${commit.createCommitOnBranch.commit.oid}' on branch - '${commit.createCommitOnBranch.ref.name}'`)
|
||||||
|
|
||||||
|
// switch back to previous branch/state since we are done with reading the changed file contents
|
||||||
|
await git.checkout('-')
|
||||||
|
|
||||||
|
} else {
|
||||||
|
await git.push([
|
||||||
|
'--force-with-lease',
|
||||||
|
branchRemoteName,
|
||||||
|
`${inputs.branch}:refs/heads/${inputs.branch}`
|
||||||
|
])
|
||||||
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,6 +166,16 @@ export class GitCommandManager {
|
||||||
return output.exitCode === 1
|
return output.exitCode === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChangedFiles(ref: string, options?: string[]): Promise<string[]> {
|
||||||
|
const args = ['diff', '--name-only']
|
||||||
|
if (options) {
|
||||||
|
args.push(...options)
|
||||||
|
}
|
||||||
|
args.push(ref)
|
||||||
|
const output = await this.exec(args)
|
||||||
|
return output.stdout.split("\n").filter((filename) => filename != '')
|
||||||
|
}
|
||||||
|
|
||||||
async isDirty(untracked: boolean, pathspec?: string[]): Promise<boolean> {
|
async isDirty(untracked: boolean, pathspec?: string[]): Promise<boolean> {
|
||||||
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
|
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
|
||||||
// Check untracked changes
|
// Check untracked changes
|
||||||
|
|
|
@ -27,7 +27,8 @@ async function run(): Promise<void> {
|
||||||
reviewers: utils.getInputAsArray('reviewers'),
|
reviewers: utils.getInputAsArray('reviewers'),
|
||||||
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
||||||
milestone: Number(core.getInput('milestone')),
|
milestone: Number(core.getInput('milestone')),
|
||||||
draft: core.getBooleanInput('draft')
|
draft: core.getBooleanInput('draft'),
|
||||||
|
signCommit: core.getBooleanInput('sign-commit'),
|
||||||
}
|
}
|
||||||
core.debug(`Inputs: ${inspect(inputs)}`)
|
core.debug(`Inputs: ${inspect(inputs)}`)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue