Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | |
| `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` |
| `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

Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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
sign-commit:
description: 'Sign the commit as github-actions bot (and as custom app if a different github-token is provided)'
default: false
outputs:
pull-request-number:
description: 'The pull request number'
Expand Down
69,041 changes: 46,994 additions & 22,047 deletions dist/index.js

Large diffs are not rendered by default.

3,118 changes: 1,592 additions & 1,526 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@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-rest-endpoint-methods": "^6.8.1",
"proxy-from-env": "^1.1.0",
Expand All @@ -55,6 +57,6 @@
"js-yaml": "^4.1.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.3",
"typescript": "^4.9.5"
"typescript": "^5.5.4"
}
}
188 changes: 183 additions & 5 deletions src/create-pull-request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
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 {
createOrUpdateBranch,
getWorkingBaseAndType,
Expand Down Expand Up @@ -32,6 +40,7 @@ export interface Inputs {
teamReviewers: string[]
milestone: number
draft: boolean
signCommit: boolean
}

export async function createPullRequest(inputs: Inputs): Promise<void> {
Expand Down Expand Up @@ -192,11 +201,180 @@ 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.signCommit) {
core.info(`Use API to push a signed commit`)
const graphqlWithAuth = graphql.defaults({
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()
}

Expand Down
10 changes: 10 additions & 0 deletions src/git-command-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ export class GitCommandManager {
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> {
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
// Check untracked changes
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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'),
signCommit: core.getBooleanInput('sign-commit'),
}
core.debug(`Inputs: ${inspect(inputs)}`)

Expand Down