signed commits via graphql
This commit is contained in:
parent
3707121594
commit
9b00b13f5b
15 changed files with 27410 additions and 2315 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
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import {
|
import {
|
||||||
createOrUpdateBranch,
|
createOrUpdateBranch,
|
||||||
tryFetch,
|
tryFetch,
|
||||||
getWorkingBaseAndType
|
getWorkingBaseAndType,
|
||||||
|
buildBranchFileChanges,
|
||||||
|
buildBranchCommits
|
||||||
} from '../lib/create-or-update-branch'
|
} from '../lib/create-or-update-branch'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {GitCommandManager} from '../lib/git-command-manager'
|
import {GitCommandManager} from '../lib/git-command-manager'
|
||||||
|
@ -229,6 +231,147 @@ describe('create-or-update-branch tests', () => {
|
||||||
expect(workingBaseType).toEqual('commit')
|
expect(workingBaseType).toEqual('commit')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchCommits with no diff', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||||
|
expect(branchCommits.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchCommits with addition and modification', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
await createChanges()
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', 'Test changes'])
|
||||||
|
|
||||||
|
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchCommits.length).toEqual(1)
|
||||||
|
expect(branchCommits[0].subject).toEqual('Test changes')
|
||||||
|
expect(branchCommits[0].changes.length).toEqual(2)
|
||||||
|
expect(branchCommits[0].changes).toEqual([
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'M'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: 'A'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchCommits with addition and deletion', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
await createChanges()
|
||||||
|
const TRACKED_FILE_NEW_PATH = 'c/tracked-file.txt'
|
||||||
|
const filepath = path.join(REPO_PATH, TRACKED_FILE_NEW_PATH)
|
||||||
|
await fs.promises.mkdir(path.dirname(filepath), {recursive: true})
|
||||||
|
await fs.promises.rename(path.join(REPO_PATH, TRACKED_FILE), filepath)
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', 'Test changes'])
|
||||||
|
|
||||||
|
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchCommits.length).toEqual(1)
|
||||||
|
expect(branchCommits[0].subject).toEqual('Test changes')
|
||||||
|
expect(branchCommits[0].changes.length).toEqual(3)
|
||||||
|
expect(branchCommits[0].changes).toEqual([
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'D'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: 'A'},
|
||||||
|
{mode: '100644', path: TRACKED_FILE_NEW_PATH, status: 'A'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchCommits with multiple commits', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await createChanges()
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', `Test changes ${i}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchCommits.length).toEqual(3)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
expect(branchCommits[i].subject).toEqual(`Test changes ${i}`)
|
||||||
|
expect(branchCommits[i].changes.length).toEqual(2)
|
||||||
|
const untrackedFileStatus = i == 0 ? 'A' : 'M'
|
||||||
|
expect(branchCommits[i].changes).toEqual([
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'M'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: untrackedFileStatus}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchFileChanges with no diff', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
const branchFileChanges = await buildBranchFileChanges(git, BASE, BRANCH)
|
||||||
|
expect(branchFileChanges.additions.length).toEqual(0)
|
||||||
|
expect(branchFileChanges.deletions.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchFileChanges with addition and modification', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
const changes = await createChanges()
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', 'Test changes'])
|
||||||
|
|
||||||
|
const branchFileChanges = await buildBranchFileChanges(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchFileChanges.additions).toEqual([
|
||||||
|
{
|
||||||
|
path: TRACKED_FILE,
|
||||||
|
contents: Buffer.from(changes.tracked, 'binary').toString('base64')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: UNTRACKED_FILE,
|
||||||
|
contents: Buffer.from(changes.untracked, 'binary').toString('base64')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(branchFileChanges.deletions.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchFileChanges with addition and deletion', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
const changes = await createChanges()
|
||||||
|
const TRACKED_FILE_NEW_PATH = 'c/tracked-file.txt'
|
||||||
|
const filepath = path.join(REPO_PATH, TRACKED_FILE_NEW_PATH)
|
||||||
|
await fs.promises.mkdir(path.dirname(filepath), {recursive: true})
|
||||||
|
await fs.promises.rename(path.join(REPO_PATH, TRACKED_FILE), filepath)
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', 'Test changes'])
|
||||||
|
|
||||||
|
const branchFileChanges = await buildBranchFileChanges(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchFileChanges.additions).toEqual([
|
||||||
|
{
|
||||||
|
path: UNTRACKED_FILE,
|
||||||
|
contents: Buffer.from(changes.untracked, 'binary').toString('base64')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: TRACKED_FILE_NEW_PATH,
|
||||||
|
contents: Buffer.from(changes.tracked, 'binary').toString('base64')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(branchFileChanges.deletions).toEqual([{path: TRACKED_FILE}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests buildBranchFileChanges with binary files', async () => {
|
||||||
|
await git.checkout(BRANCH, BASE)
|
||||||
|
const filename = 'c/untracked-binary-file'
|
||||||
|
const filepath = path.join(REPO_PATH, filename)
|
||||||
|
const binaryData = Buffer.from([0x00, 0xff, 0x10, 0x20])
|
||||||
|
await fs.promises.mkdir(path.dirname(filepath), {recursive: true})
|
||||||
|
await fs.promises.writeFile(filepath, binaryData)
|
||||||
|
await git.exec(['add', '-A'])
|
||||||
|
await git.commit(['-m', 'Test changes'])
|
||||||
|
|
||||||
|
const branchFileChanges = await buildBranchFileChanges(git, BASE, BRANCH)
|
||||||
|
|
||||||
|
expect(branchFileChanges.additions).toEqual([
|
||||||
|
{
|
||||||
|
path: filename,
|
||||||
|
contents: binaryData.toString('base64')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(branchFileChanges.deletions.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('tests no changes resulting in no new branch being created', async () => {
|
it('tests no changes resulting in no new branch being created', async () => {
|
||||||
const commitMessage = uuidv4()
|
const commitMessage = uuidv4()
|
||||||
const result = await createOrUpdateBranch(
|
const result = await createOrUpdateBranch(
|
||||||
|
|
|
@ -13,7 +13,7 @@ git daemon --verbose --enable=receive-pack --base-path=/git/remote --export-all
|
||||||
# Give the daemon time to start
|
# Give the daemon time to start
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Create a local clone and make an initial commit
|
# Create a local clone and make initial commits
|
||||||
mkdir -p /git/local/repos
|
mkdir -p /git/local/repos
|
||||||
git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base
|
git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base
|
||||||
cd /git/local/repos/test-base
|
cd /git/local/repos/test-base
|
||||||
|
@ -22,6 +22,10 @@ git config --global user.name "Your Name"
|
||||||
echo "#test-base" > README.md
|
echo "#test-base" > README.md
|
||||||
git add .
|
git add .
|
||||||
git commit -m "initial commit"
|
git commit -m "initial commit"
|
||||||
|
echo "#test-base :sparkles:" > README.md
|
||||||
|
git add .
|
||||||
|
git commit -m "add sparkles" -m "Change description:
|
||||||
|
- updates README.md to add sparkles to the title"
|
||||||
git push -u
|
git push -u
|
||||||
git log -1 --pretty=oneline
|
git log -1 --pretty=oneline
|
||||||
git config --global --unset user.email
|
git config --global --unset user.email
|
||||||
|
|
26
__test__/git-command-manager.int.test.ts
Normal file
26
__test__/git-command-manager.int.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {GitCommandManager, Commit} from '../lib/git-command-manager'
|
||||||
|
|
||||||
|
const REPO_PATH = '/git/local/repos/test-base'
|
||||||
|
|
||||||
|
describe('git-command-manager integration tests', () => {
|
||||||
|
let git: GitCommandManager
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
git = await GitCommandManager.create(REPO_PATH)
|
||||||
|
await git.checkout('main')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tests getCommit', async () => {
|
||||||
|
const parent = await git.getCommit('HEAD^')
|
||||||
|
const commit = await git.getCommit('HEAD')
|
||||||
|
expect(parent.subject).toEqual('initial commit')
|
||||||
|
expect(parent.changes).toEqual([
|
||||||
|
{mode: '100644', status: 'A', path: 'README.md'}
|
||||||
|
])
|
||||||
|
expect(commit.subject).toEqual('add sparkles')
|
||||||
|
expect(commit.parents[0]).toEqual(parent.sha)
|
||||||
|
expect(commit.changes).toEqual([
|
||||||
|
{mode: '100644', status: 'M', path: 'README.md'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,7 +7,6 @@ const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader'
|
||||||
|
|
||||||
describe('git-config-helper integration tests', () => {
|
describe('git-config-helper integration tests', () => {
|
||||||
let git: GitCommandManager
|
let git: GitCommandManager
|
||||||
let gitConfigHelper: GitConfigHelper
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
git = await GitCommandManager.create(REPO_PATH)
|
git = await GitCommandManager.create(REPO_PATH)
|
||||||
|
|
|
@ -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: true
|
||||||
outputs:
|
outputs:
|
||||||
pull-request-number:
|
pull-request-number:
|
||||||
description: 'The pull request number'
|
description: 'The pull request number'
|
||||||
|
|
25477
dist/index.js
vendored
25477
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3601
package-lock.json
generated
3601
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",
|
||||||
|
@ -41,7 +43,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^18.19.43",
|
"@types/node": "^18.19.43",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||||
|
"@typescript-eslint/parser": "^7.17.0",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
@ -55,6 +58,6 @@
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.4",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as core from '@actions/core'
|
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'
|
import {v4 as uuidv4} from 'uuid'
|
||||||
|
import * as utils from './utils'
|
||||||
|
|
||||||
const CHERRYPICK_EMPTY =
|
const CHERRYPICK_EMPTY =
|
||||||
'The previous cherry-pick is now empty, possibly due to conflict resolution.'
|
'The previous cherry-pick is now empty, possibly due to conflict resolution.'
|
||||||
|
@ -47,6 +48,56 @@ 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)
|
||||||
|
}
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildBranchFileChanges(
|
||||||
|
git: GitCommandManager,
|
||||||
|
base: string,
|
||||||
|
branch: string
|
||||||
|
): Promise<BranchFileChanges> {
|
||||||
|
const branchFileChanges: BranchFileChanges = {
|
||||||
|
additions: [],
|
||||||
|
deletions: []
|
||||||
|
}
|
||||||
|
const changedFiles = await git.getChangedFiles([
|
||||||
|
'--diff-filter=AM',
|
||||||
|
`${base}..${branch}`
|
||||||
|
])
|
||||||
|
const deletedFiles = await git.getChangedFiles([
|
||||||
|
'--diff-filter=D',
|
||||||
|
`${base}..${branch}`
|
||||||
|
])
|
||||||
|
const repoPath = git.getWorkingDirectory()
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
branchFileChanges.additions!.push({
|
||||||
|
path: file,
|
||||||
|
contents: utils.readFileBase64([repoPath, file])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const file of deletedFiles) {
|
||||||
|
branchFileChanges.deletions!.push({
|
||||||
|
path: file
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return branchFileChanges
|
||||||
|
}
|
||||||
|
|
||||||
// Return the number of commits that branch2 is ahead of branch1
|
// Return the number of commits that branch2 is ahead of branch1
|
||||||
async function commitsAhead(
|
async function commitsAhead(
|
||||||
git: GitCommandManager,
|
git: GitCommandManager,
|
||||||
|
@ -110,11 +161,23 @@ function splitLines(multilineString: string): string[] {
|
||||||
.filter(x => x !== '')
|
.filter(x => x !== '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BranchFileChanges {
|
||||||
|
additions: {
|
||||||
|
path: string
|
||||||
|
contents: string
|
||||||
|
}[]
|
||||||
|
deletions: {
|
||||||
|
path: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateOrUpdateBranchResult {
|
interface CreateOrUpdateBranchResult {
|
||||||
action: string
|
action: string
|
||||||
base: string
|
base: string
|
||||||
hasDiffWithBase: boolean
|
hasDiffWithBase: boolean
|
||||||
headSha: string
|
headSha: string
|
||||||
|
branchFileChanges?: BranchFileChanges
|
||||||
|
branchCommits: Commit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrUpdateBranch(
|
export async function createOrUpdateBranch(
|
||||||
|
@ -144,7 +207,8 @@ export async function createOrUpdateBranch(
|
||||||
action: 'none',
|
action: 'none',
|
||||||
base: base,
|
base: base,
|
||||||
hasDiffWithBase: false,
|
hasDiffWithBase: false,
|
||||||
headSha: ''
|
headSha: '',
|
||||||
|
branchCommits: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the working base changes to a temporary branch
|
// Save the working base changes to a temporary branch
|
||||||
|
@ -289,6 +353,12 @@ export async function createOrUpdateBranch(
|
||||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the branch commits
|
||||||
|
result.branchCommits = await buildBranchCommits(git, base, branch)
|
||||||
|
|
||||||
|
// Build the branch file changes
|
||||||
|
result.branchFileChanges = await buildBranchFileChanges(git, base, branch)
|
||||||
|
|
||||||
// Get the pull request branch SHA
|
// Get the pull request branch SHA
|
||||||
result.headSha = await git.revParse('HEAD')
|
result.headSha = await git.revParse('HEAD')
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,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> {
|
||||||
|
@ -185,6 +186,8 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
inputs.signoff,
|
inputs.signoff,
|
||||||
inputs.addPaths
|
inputs.addPaths
|
||||||
)
|
)
|
||||||
|
// Set the base. It would have been '' if not specified as an input
|
||||||
|
inputs.base = result.base
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
if (['created', 'updated'].includes(result.action)) {
|
if (['created', 'updated'].includes(result.action)) {
|
||||||
|
@ -192,17 +195,37 @@ 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',
|
// Stash any uncommitted tracked and untracked changes
|
||||||
branchRemoteName,
|
const stashed = await git.stashPush(['--include-untracked'])
|
||||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
await git.checkout(inputs.branch)
|
||||||
])
|
await githubHelper.pushSignedCommits(
|
||||||
|
result.branchCommits,
|
||||||
|
repoPath,
|
||||||
|
branchRepository,
|
||||||
|
inputs.branch
|
||||||
|
)
|
||||||
|
// await githubHelper.pushSignedCommit(
|
||||||
|
// branchRepository,
|
||||||
|
// inputs.branch,
|
||||||
|
// inputs.base,
|
||||||
|
// inputs.commitMessage,
|
||||||
|
// result.branchFileChanges
|
||||||
|
// )
|
||||||
|
await git.checkout('-')
|
||||||
|
if (stashed) {
|
||||||
|
await git.stashPop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await git.push([
|
||||||
|
'--force-with-lease',
|
||||||
|
branchRemoteName,
|
||||||
|
`${inputs.branch}:refs/heads/${inputs.branch}`
|
||||||
|
])
|
||||||
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the base. It would have been '' if not specified as an input
|
|
||||||
inputs.base = result.base
|
|
||||||
|
|
||||||
if (result.hasDiffWithBase) {
|
if (result.hasDiffWithBase) {
|
||||||
// Create or update the pull request
|
// Create or update the pull request
|
||||||
core.startGroup('Create or update the pull request')
|
core.startGroup('Create or update the pull request')
|
||||||
|
|
|
@ -5,6 +5,19 @@ import * as path from 'path'
|
||||||
|
|
||||||
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||||
|
|
||||||
|
export type Commit = {
|
||||||
|
sha: string
|
||||||
|
tree: string
|
||||||
|
parents: string[]
|
||||||
|
subject: string
|
||||||
|
body: string
|
||||||
|
changes: {
|
||||||
|
mode: string
|
||||||
|
status: 'A' | 'M' | 'D'
|
||||||
|
path: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
export class GitCommandManager {
|
export class GitCommandManager {
|
||||||
private gitPath: string
|
private gitPath: string
|
||||||
private workingDirectory: string
|
private workingDirectory: string
|
||||||
|
@ -138,6 +151,43 @@ export class GitCommandManager {
|
||||||
await this.exec(args)
|
await this.exec(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCommit(ref: string): Promise<Commit> {
|
||||||
|
const endOfBody = '###EOB###'
|
||||||
|
const output = await this.exec([
|
||||||
|
'show',
|
||||||
|
'--raw',
|
||||||
|
'--cc',
|
||||||
|
'--diff-filter=AMD',
|
||||||
|
`--format=%H%n%T%n%P%n%s%n%b%n${endOfBody}`,
|
||||||
|
ref
|
||||||
|
])
|
||||||
|
const lines = output.stdout.split('\n')
|
||||||
|
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
|
||||||
|
const detailLines = lines.slice(0, endOfBodyIndex)
|
||||||
|
|
||||||
|
return <Commit>{
|
||||||
|
sha: detailLines[0],
|
||||||
|
tree: detailLines[1],
|
||||||
|
parents: detailLines[2].split(' '),
|
||||||
|
subject: detailLines[3],
|
||||||
|
body: detailLines.slice(4, 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 {
|
||||||
|
throw new Error(`Unexpected line format: ${line}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||||
const output = await this.exec([
|
const output = await this.exec([
|
||||||
'config',
|
'config',
|
||||||
|
@ -166,6 +216,15 @@ export class GitCommandManager {
|
||||||
return output.exitCode === 1
|
return output.exitCode === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChangedFiles(options?: string[]): Promise<string[]> {
|
||||||
|
const args = ['diff', '--name-only']
|
||||||
|
if (options) {
|
||||||
|
args.push(...options)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {Inputs} from './create-pull-request'
|
import {Inputs} from './create-pull-request'
|
||||||
|
import {Commit} from './git-command-manager'
|
||||||
import {Octokit, OctokitOptions} from './octokit-client'
|
import {Octokit, OctokitOptions} from './octokit-client'
|
||||||
|
import type {
|
||||||
|
Repository as TempRepository,
|
||||||
|
Ref,
|
||||||
|
Commit as CommitTemp,
|
||||||
|
FileChanges
|
||||||
|
} from '@octokit/graphql-schema'
|
||||||
|
import {BranchFileChanges} from './create-or-update-branch'
|
||||||
import * as utils from './utils'
|
import * as utils from './utils'
|
||||||
|
|
||||||
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
||||||
|
@ -17,6 +25,13 @@ interface Pull {
|
||||||
created: boolean
|
created: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TreeObject = {
|
||||||
|
path: string
|
||||||
|
mode: '100644' | '100755' | '040000' | '160000' | '120000'
|
||||||
|
sha: string | null
|
||||||
|
type: 'blob'
|
||||||
|
}
|
||||||
|
|
||||||
export class GitHubHelper {
|
export class GitHubHelper {
|
||||||
private octokit: InstanceType<typeof Octokit>
|
private octokit: InstanceType<typeof Octokit>
|
||||||
|
|
||||||
|
@ -184,4 +199,268 @@ export class GitHubHelper {
|
||||||
|
|
||||||
return pull
|
return pull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pushSignedCommits(
|
||||||
|
branchCommits: Commit[],
|
||||||
|
repoPath: string,
|
||||||
|
branchRepository: string,
|
||||||
|
branch: string
|
||||||
|
): Promise<void> {
|
||||||
|
let headSha = ''
|
||||||
|
for (const commit of branchCommits) {
|
||||||
|
headSha = await this.createCommit(commit, repoPath, branchRepository)
|
||||||
|
}
|
||||||
|
await this.createOrUpdateRef(branchRepository, branch, headSha)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createCommit(
|
||||||
|
commit: Commit,
|
||||||
|
repoPath: string,
|
||||||
|
branchRepository: string
|
||||||
|
): Promise<string> {
|
||||||
|
const repository = this.parseRepository(branchRepository)
|
||||||
|
let treeSha = commit.tree
|
||||||
|
if (commit.changes.length > 0) {
|
||||||
|
core.debug(`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') {
|
||||||
|
core.debug(`Creating blob for file '${path}'`)
|
||||||
|
const {data: blob} = await this.octokit.rest.git.createBlob({
|
||||||
|
...repository,
|
||||||
|
content: utils.readFileBase64([repoPath, path]),
|
||||||
|
encoding: 'base64'
|
||||||
|
})
|
||||||
|
sha = blob.sha
|
||||||
|
}
|
||||||
|
return <TreeObject>{
|
||||||
|
path,
|
||||||
|
mode,
|
||||||
|
sha,
|
||||||
|
type: 'blob'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
core.debug(`Creating tree for local commit ${commit.sha}`)
|
||||||
|
const {data: tree} = await this.octokit.rest.git.createTree({
|
||||||
|
...repository,
|
||||||
|
base_tree: commit.parents[0],
|
||||||
|
tree: treeObjects
|
||||||
|
})
|
||||||
|
treeSha = tree.sha
|
||||||
|
core.debug(`Created tree ${treeSha} for local commit ${commit.sha}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
|
||||||
|
...repository,
|
||||||
|
parents: commit.parents,
|
||||||
|
tree: treeSha,
|
||||||
|
message: `${commit.subject}\n\n${commit.body}`
|
||||||
|
})
|
||||||
|
core.debug(
|
||||||
|
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
|
||||||
|
)
|
||||||
|
return remoteCommit.sha
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createOrUpdateRef(
|
||||||
|
branchRepository: string,
|
||||||
|
branch: string,
|
||||||
|
newHead: string
|
||||||
|
) {
|
||||||
|
const repository = this.parseRepository(branchRepository)
|
||||||
|
const branchExists = await this.octokit.rest.git
|
||||||
|
.getRef({
|
||||||
|
...repository,
|
||||||
|
ref: branch
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
() => true,
|
||||||
|
() => false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (branchExists) {
|
||||||
|
core.debug(`Branch ${branch} exists, updating ref`)
|
||||||
|
await this.octokit.rest.git.updateRef({
|
||||||
|
...repository,
|
||||||
|
sha: newHead,
|
||||||
|
ref: `heads/${branch}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
core.debug(`Branch ${branch} does not exist, creating ref`)
|
||||||
|
await this.octokit.rest.git.createRef({
|
||||||
|
...repository,
|
||||||
|
sha: newHead,
|
||||||
|
ref: `refs/heads/${branch}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushSignedCommit(
|
||||||
|
branchRepository: string,
|
||||||
|
branch: string,
|
||||||
|
base: string,
|
||||||
|
commitMessage: string,
|
||||||
|
branchFileChanges?: BranchFileChanges
|
||||||
|
): Promise<void> {
|
||||||
|
core.info(`Use API to push a signed commit`)
|
||||||
|
|
||||||
|
const [repoOwner, repoName] = branchRepository.split('/')
|
||||||
|
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 this.octokit.graphql<{repository: TempRepository}>(
|
||||||
|
refQuery,
|
||||||
|
{
|
||||||
|
repoOwner: repoOwner,
|
||||||
|
repoName: repoName,
|
||||||
|
branchName: branch
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug(
|
||||||
|
`Fetched information for branch '${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 - '${branch}'`)
|
||||||
|
branchRef = await this.octokit.graphql<{repository: TempRepository}>(
|
||||||
|
refQuery,
|
||||||
|
{
|
||||||
|
repoOwner: repoOwner,
|
||||||
|
repoName: repoName,
|
||||||
|
branchName: base
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug(
|
||||||
|
`Fetched information for base branch '${base}' - '${JSON.stringify(branchRef)}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Creating new branch '${branch}' from '${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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const newBranch = await this.octokit.graphql<{createRef: {ref: Ref}}>(
|
||||||
|
newBranchMutation,
|
||||||
|
{
|
||||||
|
repoId: branchRef.repository.id,
|
||||||
|
oid: branchRef.repository.ref.target!.oid,
|
||||||
|
branchName: 'refs/heads/' + branch
|
||||||
|
}
|
||||||
|
)
|
||||||
|
core.debug(
|
||||||
|
`Created new branch '${branch}': '${JSON.stringify(newBranch.createRef.ref)}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.info(
|
||||||
|
`Hash ref of branch '${branch}' is '${JSON.stringify(branchRef.repository.ref!.target!.oid)}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
const fileChanges = <FileChanges>{
|
||||||
|
additions: branchFileChanges!.additions,
|
||||||
|
deletions: branchFileChanges!.deletions
|
||||||
|
}
|
||||||
|
|
||||||
|
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: branch,
|
||||||
|
repoNameWithOwner: repoOwner + '/' + repoName,
|
||||||
|
headOid: branchRef.repository.ref!.target!.oid,
|
||||||
|
commitMessage: commitMessage,
|
||||||
|
fileChanges: fileChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushCommitVarsWithoutContents = {
|
||||||
|
...pushCommitVars,
|
||||||
|
fileChanges: {
|
||||||
|
...pushCommitVars.fileChanges,
|
||||||
|
additions: pushCommitVars.fileChanges.additions?.map(addition => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {contents, ...rest} = addition
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`Push commit with payload: '${JSON.stringify(pushCommitVarsWithoutContents)}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
const commit = await this.octokit.graphql<{
|
||||||
|
createCommitOnBranch: {ref: Ref; commit: CommitTemp}
|
||||||
|
}>(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}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)}`)
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,10 @@ export function readFile(path: string): string {
|
||||||
return fs.readFileSync(path, 'utf-8')
|
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 */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
function hasErrorCode(error: any): error is {code: string} {
|
function hasErrorCode(error: any): error is {code: string} {
|
||||||
return typeof (error && error.code) === 'string'
|
return typeof (error && error.code) === 'string'
|
||||||
|
|
Loading…
Reference in a new issue