Skip to content

Commit 70815fe

Browse files
rustycl0ckpeter-evans
authored andcommitted
Add support for signed commits (#3055)
1 parent 93bc7fd commit 70815fe

File tree

8 files changed

+48785
-23577
lines changed

8 files changed

+48785
-23577
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ All inputs are **optional**. If not set, sensible defaults will be used.
7474
| `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. | |
7575
| `milestone` | The number of the milestone to associate this pull request with. | |
7676
| `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` |
77+
| `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` |
7778

7879
#### commit-message
7980

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ inputs:
7474
draft:
7575
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
7676
default: false
77+
sign-commit:
78+
description: 'Sign the commit as github-actions bot (and as custom app if a different github-token is provided)'
79+
default: false
7780
outputs:
7881
pull-request-number:
7982
description: 'The pull request number'

dist/index.js

Lines changed: 46994 additions & 22047 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 1589 additions & 1523 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"@actions/core": "^1.10.1",
3333
"@actions/exec": "^1.1.1",
3434
"@octokit/core": "^4.2.4",
35+
"@octokit/graphql": "^8.1.1",
36+
"@octokit/graphql-schema": "^15.25.0",
3537
"@octokit/plugin-paginate-rest": "^5.0.1",
3638
"@octokit/plugin-rest-endpoint-methods": "^6.8.1",
3739
"proxy-from-env": "^1.1.0",
@@ -55,6 +57,6 @@
5557
"js-yaml": "^4.1.0",
5658
"prettier": "^3.3.3",
5759
"ts-jest": "^29.2.3",
58-
"typescript": "^4.9.5"
60+
"typescript": "^5.5.4"
5961
}
6062
}

src/create-pull-request.ts

Lines changed: 183 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import * as core from '@actions/core'
2+
import * as fs from 'fs'
3+
import { graphql } from '@octokit/graphql'
4+
import type {
5+
Repository,
6+
Ref,
7+
Commit,
8+
FileChanges
9+
} from '@octokit/graphql-schema'
210
import {
311
createOrUpdateBranch,
412
getWorkingBaseAndType,
@@ -32,6 +40,7 @@ export interface Inputs {
3240
teamReviewers: string[]
3341
milestone: number
3442
draft: boolean
43+
signCommit: boolean
3544
}
3645

3746
export async function createPullRequest(inputs: Inputs): Promise<void> {
@@ -192,11 +201,180 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
192201
core.startGroup(
193202
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
194203
)
195-
await git.push([
196-
'--force-with-lease',
197-
branchRemoteName,
198-
`${inputs.branch}:refs/heads/${inputs.branch}`
199-
])
204+
if (inputs.signCommit) {
205+
core.info(`Use API to push a signed commit`)
206+
const graphqlWithAuth = graphql.defaults({
207+
headers: {
208+
authorization: 'token ' + inputs.token,
209+
},
210+
});
211+
212+
let repoOwner = process.env.GITHUB_REPOSITORY!.split("/")[0]
213+
if (inputs.pushToFork) {
214+
const forkName = await githubHelper.getRepositoryParent(baseRemote.repository)
215+
if (!forkName) { repoOwner = forkName! }
216+
}
217+
const repoName = process.env.GITHUB_REPOSITORY!.split("/")[1]
218+
219+
core.debug(`repoOwner: '${repoOwner}', repoName: '${repoName}'`)
220+
const refQuery = `
221+
query GetRefId($repoName: String!, $repoOwner: String!, $branchName: String!) {
222+
repository(owner: $repoOwner, name: $repoName){
223+
id
224+
ref(qualifiedName: $branchName){
225+
id
226+
name
227+
prefix
228+
target{
229+
id
230+
oid
231+
commitUrl
232+
commitResourcePath
233+
abbreviatedOid
234+
}
235+
}
236+
},
237+
}
238+
`
239+
240+
let branchRef = await graphqlWithAuth<{repository: Repository}>(
241+
refQuery,
242+
{
243+
repoOwner: repoOwner,
244+
repoName: repoName,
245+
branchName: inputs.branch
246+
}
247+
)
248+
core.debug( `Fetched information for branch '${inputs.branch}' - '${JSON.stringify(branchRef)}'`)
249+
250+
// if the branch does not exist, then first we need to create the branch from base
251+
if (branchRef.repository.ref == null) {
252+
core.debug( `Branch does not exist - '${inputs.branch}'`)
253+
branchRef = await graphqlWithAuth<{repository: Repository}>(
254+
refQuery,
255+
{
256+
repoOwner: repoOwner,
257+
repoName: repoName,
258+
branchName: inputs.base
259+
}
260+
)
261+
core.debug( `Fetched information for base branch '${inputs.base}' - '${JSON.stringify(branchRef)}'`)
262+
263+
core.info( `Creating new branch '${inputs.branch}' from '${inputs.base}', with ref '${JSON.stringify(branchRef.repository.ref!.target!.oid)}'`)
264+
if (branchRef.repository.ref != null) {
265+
core.debug( `Send request for creating new branch`)
266+
const newBranchMutation = `
267+
mutation CreateNewBranch($branchName: String!, $oid: GitObjectID!, $repoId: ID!) {
268+
createRef(input: {
269+
name: $branchName,
270+
oid: $oid,
271+
repositoryId: $repoId
272+
}) {
273+
ref {
274+
id
275+
name
276+
prefix
277+
}
278+
}
279+
}
280+
`
281+
let newBranch = await graphqlWithAuth<{createRef: {ref: Ref}}>(
282+
newBranchMutation,
283+
{
284+
repoId: branchRef.repository.id,
285+
oid: branchRef.repository.ref.target!.oid,
286+
branchName: 'refs/heads/' + inputs.branch
287+
}
288+
)
289+
core.debug(`Created new branch '${inputs.branch}': '${JSON.stringify(newBranch.createRef.ref)}'`)
290+
}
291+
}
292+
core.info( `Hash ref of branch '${inputs.branch}' is '${JSON.stringify(branchRef.repository.ref!.target!.oid)}'`)
293+
294+
// switch to input-branch for reading updated file contents
295+
await git.checkout(inputs.branch)
296+
297+
let changedFiles = await git.getChangedFiles(branchRef.repository.ref!.target!.oid, ['--diff-filter=M'])
298+
let deletedFiles = await git.getChangedFiles(branchRef.repository.ref!.target!.oid, ['--diff-filter=D'])
299+
let fileChanges = <FileChanges>{additions: [], deletions: []}
300+
301+
core.debug(`Changed files: '${JSON.stringify(changedFiles)}'`)
302+
core.debug(`Deleted files: '${JSON.stringify(deletedFiles)}'`)
303+
304+
for (var file of changedFiles) {
305+
fileChanges.additions!.push({
306+
path: file,
307+
contents: btoa(fs.readFileSync(file, 'utf8')),
308+
})
309+
}
310+
311+
for (var file of deletedFiles) {
312+
fileChanges.deletions!.push({
313+
path: file,
314+
})
315+
}
316+
317+
const pushCommitMutation = `
318+
mutation PushCommit(
319+
$repoNameWithOwner: String!,
320+
$branchName: String!,
321+
$headOid: GitObjectID!,
322+
$commitMessage: String!,
323+
$fileChanges: FileChanges
324+
) {
325+
createCommitOnBranch(input: {
326+
branch: {
327+
repositoryNameWithOwner: $repoNameWithOwner,
328+
branchName: $branchName,
329+
}
330+
fileChanges: $fileChanges
331+
message: {
332+
headline: $commitMessage
333+
}
334+
expectedHeadOid: $headOid
335+
}){
336+
clientMutationId
337+
ref{
338+
id
339+
name
340+
prefix
341+
}
342+
commit{
343+
id
344+
abbreviatedOid
345+
oid
346+
}
347+
}
348+
}
349+
`
350+
const pushCommitVars = {
351+
branchName: inputs.branch,
352+
repoNameWithOwner: repoOwner + '/' + repoName,
353+
headOid: branchRef.repository.ref!.target!.oid,
354+
commitMessage: inputs.commitMessage,
355+
fileChanges: fileChanges,
356+
}
357+
358+
core.info(`Push commit with payload: '${JSON.stringify(pushCommitVars)}'`)
359+
360+
const commit = await graphqlWithAuth<{createCommitOnBranch: {ref: Ref, commit: Commit} }>(
361+
pushCommitMutation,
362+
pushCommitVars,
363+
);
364+
365+
core.debug( `Pushed commit - '${JSON.stringify(commit)}'`)
366+
core.info( `Pushed commit with hash - '${commit.createCommitOnBranch.commit.oid}' on branch - '${commit.createCommitOnBranch.ref.name}'`)
367+
368+
// switch back to previous branch/state since we are done with reading the changed file contents
369+
await git.checkout('-')
370+
371+
} else {
372+
await git.push([
373+
'--force-with-lease',
374+
branchRemoteName,
375+
`${inputs.branch}:refs/heads/${inputs.branch}`
376+
])
377+
}
200378
core.endGroup()
201379
}
202380

src/git-command-manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ export class GitCommandManager {
166166
return output.exitCode === 1
167167
}
168168

169+
async getChangedFiles(ref: string, options?: string[]): Promise<string[]> {
170+
const args = ['diff', '--name-only']
171+
if (options) {
172+
args.push(...options)
173+
}
174+
args.push(ref)
175+
const output = await this.exec(args)
176+
return output.stdout.split("\n").filter((filename) => filename != '')
177+
}
178+
169179
async isDirty(untracked: boolean, pathspec?: string[]): Promise<boolean> {
170180
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
171181
// Check untracked changes

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ async function run(): Promise<void> {
2727
reviewers: utils.getInputAsArray('reviewers'),
2828
teamReviewers: utils.getInputAsArray('team-reviewers'),
2929
milestone: Number(core.getInput('milestone')),
30-
draft: core.getBooleanInput('draft')
30+
draft: core.getBooleanInput('draft'),
31+
signCommit: core.getBooleanInput('sign-commit'),
3132
}
3233
core.debug(`Inputs: ${inspect(inputs)}`)
3334

0 commit comments

Comments
 (0)