refactor(create-pull-request): extract methods to adhere to SRP and enhance maintainability

Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
This commit is contained in:
Emilien Escalle 2025-02-26 10:32:22 +01:00
parent 271a8d0340
commit 9d92b03fa0
4 changed files with 637 additions and 446 deletions

432
dist/index.js vendored
View File

@ -397,204 +397,22 @@ function createPullRequest(inputs) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let gitConfigHelper, git; let gitConfigHelper, git;
try { try {
core.startGroup('Prepare git configuration'); const { git, gitConfigHelper, repoPath } = yield prepareGitConfiguration(inputs);
const repoPath = utils.getRepoPath(inputs.path); const { branchRemoteName, ghBranch, branchRepository, ghPull, baseRemote } = yield initializePullRequestContext(inputs, gitConfigHelper, git);
git = yield git_command_manager_1.GitCommandManager.create(repoPath);
gitConfigHelper = yield git_config_helper_1.GitConfigHelper.create(git);
core.endGroup();
core.startGroup('Determining the base and head repositories');
const baseRemote = gitConfigHelper.getGitRemote();
// Init the GitHub clients
const ghBranch = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.branchToken);
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
? inputs.pushToFork
: baseRemote.repository;
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 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.`);
}
if (branchParentRepository != baseRemote.repository &&
baseParentRepository != branchParentRepository) {
throw new Error(`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`);
}
// Add a remote for the fork
const remoteUrl = utils.getRemoteUrl(baseRemote.protocol, baseRemote.hostname, branchRepository);
yield git.exec(['remote', 'add', 'fork', remoteUrl]);
}
core.endGroup();
core.info(`Pull request branch target repository set to ${branchRepository}`);
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication');
yield gitConfigHelper.configureToken(inputs.branchToken);
core.endGroup();
}
core.startGroup('Checking the base repository state');
const [workingBase, workingBaseType] = yield (0, create_or_update_branch_1.getWorkingBaseAndType)(git);
core.info(`Working base is ${workingBaseType} '${workingBase}'`);
// When in detached HEAD state (checked out on a commit), we need to
// know the 'base' branch in order to rebase changes.
if (workingBaseType == create_or_update_branch_1.WorkingBaseType.Commit && !inputs.base) {
throw new Error(`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`);
}
// If the base is not specified it is assumed to be the working base.
const base = inputs.base ? inputs.base : workingBase;
// Throw an error if the base and branch are not different branches
// of the 'origin' remote. An identically named branch in the `fork`
// remote is perfectly fine.
if (branchRemoteName == 'origin' && base == inputs.branch) {
throw new Error(`The 'base' and 'branch' for a pull request must be different branches. Unable to continue.`);
}
// For self-hosted runners the repository state persists between runs.
// This command prunes the stale remote ref when the pull request branch was
// deleted after being merged or closed. Without this the push using
// '--force-with-lease' fails due to "stale info."
// https://github.com/peter-evans/create-pull-request/issues/633
yield git.exec(['remote', 'prune', branchRemoteName]);
core.endGroup();
// Apply the branch suffix if set // Apply the branch suffix if set
if (inputs.branchSuffix) { yield defineInputBranch(inputs, git);
switch (inputs.branchSuffix) {
case 'short-commit-hash':
// Suffix with the short SHA1 hash
inputs.branch = `${inputs.branch}-${yield git.revParse('HEAD', [
'--short'
])}`;
break;
case 'timestamp':
// Suffix with the current timestamp
inputs.branch = `${inputs.branch}-${utils.secondsSinceEpoch()}`;
break;
case 'random':
// Suffix with a 7 character random string
inputs.branch = `${inputs.branch}-${utils.randomString()}`;
break;
default:
throw new Error(`Branch suffix '${inputs.branchSuffix}' is not a valid value. Unable to continue.`);
}
}
// Output head branch // Output head branch
core.info(`Pull request branch to create or update set to '${inputs.branch}'`); core.info(`Pull request branch to create or update set to '${inputs.branch}'`);
// Configure the committer and author // Configure the committer and author
core.startGroup('Configuring the committer and author'); configureGitIdentity(inputs, git);
const parsedAuthor = utils.parseDisplayNameEmail(inputs.author);
const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer);
git.setIdentityGitOptions([
'-c',
`author.name=${parsedAuthor.name}`,
'-c',
`author.email=${parsedAuthor.email}`,
'-c',
`committer.name=${parsedCommitter.name}`,
'-c',
`committer.email=${parsedCommitter.email}`
]);
core.info(`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`);
core.info(`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`);
core.endGroup();
// Action outputs // Action outputs
const outputs = new Map(); const outputs = new Map();
outputs.set('pull-request-branch', inputs.branch); outputs.set('pull-request-branch', inputs.branch);
outputs.set('pull-request-operation', 'none'); outputs.set('pull-request-operation', 'none');
// Create or update the pull request branch // Create or update the pull request branch
core.startGroup('Create or update the pull request branch'); const result = yield createOrUpdatePullRequestBranch(git, inputs, branchRemoteName);
const result = yield (0, create_or_update_branch_1.createOrUpdateBranch)(git, inputs.commitMessage, inputs.base, inputs.branch, branchRemoteName, inputs.signoff, inputs.addPaths); yield processPullRequestBranch(result, branchRemoteName, inputs, git, ghBranch, repoPath, branchRepository, outputs, ghPull, baseRemote);
outputs.set('pull-request-head-sha', result.headSha); yield exportOutputs(outputs, result, ghBranch, branchRepository);
// Set the base. It would have been '' if not specified as an input
inputs.base = result.base;
core.endGroup();
if (['created', 'updated'].includes(result.action)) {
// The branch was created or updated
core.startGroup(`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`);
if (inputs.signCommits) {
// Create signed commits via the GitHub API
const stashed = yield git.stashPush(['--include-untracked']);
yield git.checkout(inputs.branch);
const pushSignedCommitsResult = yield ghBranch.pushSignedCommits(git, result.branchCommits, result.baseCommit, 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();
}
}
else {
yield git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
]);
}
core.endGroup();
}
if (result.hasDiffWithBase) {
core.startGroup('Create or update the pull request');
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) {
outputs.set('pull-request-operation', 'created');
}
else if (result.action == 'updated') {
outputs.set('pull-request-operation', 'updated');
// The pull request was updated AND the branch was updated.
// Convert back to draft if 'draft: always-true' is set.
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
yield ghPull.convertToDraft(pull.node_id);
}
}
core.endGroup();
}
else {
// There is no longer a diff with the base
// Check we are in a state where a branch exists
if (['updated', 'not-updated'].includes(result.action)) {
core.info(`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`);
if (inputs.deleteBranch) {
core.info(`Deleting branch '${inputs.branch}'`);
yield git.push([
'--delete',
'--force',
branchRemoteName,
`refs/heads/${inputs.branch}`
]);
outputs.set('pull-request-operation', 'closed');
}
}
}
core.startGroup('Setting outputs');
// If the head commit is signed, get its verification status if we don't already know it.
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
if (!outputs.has('pull-request-commits-verified') &&
result.branchCommits.length > 0 &&
result.branchCommits[result.branchCommits.length - 1].signed) {
// Using the local head commit SHA because in this case commits have not been pushed via the API.
core.info(`Checking verification status of head commit ${result.headSha}`);
try {
const headCommit = yield ghBranch.getCommit(result.headSha, branchRepository);
outputs.set('pull-request-commits-verified', headCommit.verified.toString());
}
catch (error) {
core.warning('Failed to check verification status of head commit.');
core.debug(utils.getErrorMessage(error));
}
}
if (!outputs.has('pull-request-commits-verified')) {
outputs.set('pull-request-commits-verified', 'false');
}
// Set outputs
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`);
core.setOutput(key, value);
}
core.endGroup();
} }
catch (error) { catch (error) {
core.setFailed(utils.getErrorMessage(error)); core.setFailed(utils.getErrorMessage(error));
@ -602,13 +420,245 @@ function createPullRequest(inputs) {
finally { finally {
core.startGroup('Restore git configuration'); core.startGroup('Restore git configuration');
if (inputs.pushToFork) { if (inputs.pushToFork) {
yield git.exec(['remote', 'rm', 'fork']); yield (git === null || git === void 0 ? void 0 : git.exec(['remote', 'rm', 'fork']));
} }
yield gitConfigHelper.close(); yield (gitConfigHelper === null || gitConfigHelper === void 0 ? void 0 : gitConfigHelper.close());
core.endGroup(); core.endGroup();
} }
}); });
} }
function prepareGitConfiguration(inputs) {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup('Prepare git configuration');
const repoPath = utils.getRepoPath(inputs.path);
const git = yield git_command_manager_1.GitCommandManager.create(repoPath);
const gitConfigHelper = yield git_config_helper_1.GitConfigHelper.create(git);
core.endGroup();
return {
repoPath,
git,
gitConfigHelper
};
});
}
function initializePullRequestContext(inputs, gitConfigHelper, git) {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup('Determining the base and head repositories');
const baseRemote = gitConfigHelper.getGitRemote();
// Init the GitHub clients
const ghBranch = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.branchToken);
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
? inputs.pushToFork
: baseRemote.repository;
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 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.`);
}
if (branchParentRepository != baseRemote.repository &&
baseParentRepository != branchParentRepository) {
throw new Error(`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`);
}
// Add a remote for the fork
const remoteUrl = utils.getRemoteUrl(baseRemote.protocol, baseRemote.hostname, branchRepository);
yield git.exec(['remote', 'add', 'fork', remoteUrl]);
}
core.endGroup();
core.info(`Pull request branch target repository set to ${branchRepository}`);
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication');
yield gitConfigHelper.configureToken(inputs.branchToken);
core.endGroup();
}
core.startGroup('Checking the base repository state');
const [workingBase, workingBaseType] = yield (0, create_or_update_branch_1.getWorkingBaseAndType)(git);
core.info(`Working base is ${workingBaseType} '${workingBase}'`);
// When in detached HEAD state (checked out on a commit), we need to
// know the 'base' branch in order to rebase changes.
if (workingBaseType == create_or_update_branch_1.WorkingBaseType.Commit && !inputs.base) {
throw new Error(`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`);
}
// If the base is not specified it is assumed to be the working base.
const base = inputs.base ? inputs.base : workingBase;
// Throw an error if the base and branch are not different branches
// of the 'origin' remote. An identically named branch in the `fork`
// remote is perfectly fine.
if (branchRemoteName == 'origin' && base == inputs.branch) {
throw new Error(`The 'base' and 'branch' for a pull request must be different branches. Unable to continue.`);
}
// For self-hosted runners the repository state persists between runs.
// This command prunes the stale remote ref when the pull request branch was
// deleted after being merged or closed. Without this the push using
// '--force-with-lease' fails due to "stale info."
// https://github.com/peter-evans/create-pull-request/issues/633
yield git.exec(['remote', 'prune', branchRemoteName]);
core.endGroup();
return { branchRemoteName, branchRepository, baseRemote, ghBranch, ghPull };
});
}
function defineInputBranch(inputs, git) {
return __awaiter(this, void 0, void 0, function* () {
if (inputs.branchSuffix) {
switch (inputs.branchSuffix) {
case 'short-commit-hash':
// Suffix with the short SHA1 hash
inputs.branch = `${inputs.branch}-${yield git.revParse('HEAD', [
'--short'
])}`;
break;
case 'timestamp':
// Suffix with the current timestamp
inputs.branch = `${inputs.branch}-${utils.secondsSinceEpoch()}`;
break;
case 'random':
// Suffix with a 7 character random string
inputs.branch = `${inputs.branch}-${utils.randomString()}`;
break;
default:
throw new Error(`Branch suffix '${inputs.branchSuffix}' is not a valid value. Unable to continue.`);
}
}
});
}
function configureGitIdentity(inputs, git) {
core.startGroup('Configuring the committer and author');
const parsedAuthor = utils.parseDisplayNameEmail(inputs.author);
const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer);
git.setIdentityGitOptions([
'-c',
`author.name=${parsedAuthor.name}`,
'-c',
`author.email=${parsedAuthor.email}`,
'-c',
`committer.name=${parsedCommitter.name}`,
'-c',
`committer.email=${parsedCommitter.email}`
]);
core.info(`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`);
core.info(`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`);
core.endGroup();
}
function createOrUpdatePullRequestBranch(git, inputs, branchRemoteName) {
return __awaiter(this, void 0, void 0, function* () {
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);
core.endGroup();
return result;
});
}
function processPullRequestBranch(result, branchRemoteName, inputs, git, ghBranch, repoPath, branchRepository, outputs, ghPull, baseRemote) {
return __awaiter(this, void 0, void 0, function* () {
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;
if (['created', 'updated'].includes(result.action)) {
// The branch was created or updated
yield pushPullRequestBranch(branchRemoteName, inputs, git, ghBranch, result, repoPath, branchRepository, outputs);
}
if (result.hasDiffWithBase) {
yield createOrUpdatePullRequest(ghPull, inputs, baseRemote, branchRepository, outputs, result);
}
else {
// There is no longer a diff with the base
// Check we are in a state where a branch exists
if (['updated', 'not-updated'].includes(result.action)) {
core.info(`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`);
if (inputs.deleteBranch) {
core.info(`Deleting branch '${inputs.branch}'`);
yield git.push([
'--delete',
'--force',
branchRemoteName,
`refs/heads/${inputs.branch}`
]);
outputs.set('pull-request-operation', 'closed');
}
}
}
});
}
function createOrUpdatePullRequest(ghPull, inputs, baseRemote, branchRepository, outputs, result) {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup('Create or update the pull request');
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) {
outputs.set('pull-request-operation', 'created');
}
else if (result.action == 'updated') {
outputs.set('pull-request-operation', 'updated');
// The pull request was updated AND the branch was updated.
// Convert back to draft if 'draft: always-true' is set.
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
yield ghPull.convertToDraft(pull.node_id);
}
}
core.endGroup();
});
}
function pushPullRequestBranch(branchRemoteName, inputs, git, ghBranch, result, repoPath, branchRepository, outputs) {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup(`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`);
if (inputs.signCommits) {
// Create signed commits via the GitHub API
const stashed = yield git.stashPush(['--include-untracked']);
yield git.checkout(inputs.branch);
const pushSignedCommitsResult = yield ghBranch.pushSignedCommits(git, result.branchCommits, result.baseCommit, 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();
}
}
else {
yield git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
]);
}
core.endGroup();
});
}
function exportOutputs(outputs, result, ghBranch, branchRepository) {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup('Setting outputs');
// If the head commit is signed, get its verification status if we don't already know it.
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
if (!outputs.has('pull-request-commits-verified') &&
result.branchCommits.length > 0 &&
result.branchCommits[result.branchCommits.length - 1].signed) {
// Using the local head commit SHA because in this case commits have not been pushed via the API.
core.info(`Checking verification status of head commit ${result.headSha}`);
try {
const headCommit = yield ghBranch.getCommit(result.headSha, branchRepository);
outputs.set('pull-request-commits-verified', headCommit.verified.toString());
}
catch (error) {
core.warning('Failed to check verification status of head commit.');
core.debug(utils.getErrorMessage(error));
}
}
if (!outputs.has('pull-request-commits-verified')) {
outputs.set('pull-request-commits-verified', 'false');
}
// Set outputs
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`);
core.setOutput(key, value);
}
core.endGroup();
});
}
/***/ }), /***/ }),

View File

@ -157,7 +157,7 @@ function splitLines(multilineString: string): string[] {
.filter(x => x !== '') .filter(x => x !== '')
} }
interface CreateOrUpdateBranchResult { export interface CreateOrUpdateBranchResult {
action: string action: string
base: string base: string
hasDiffWithBase: boolean hasDiffWithBase: boolean

View File

@ -1,12 +1,13 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { import {
createOrUpdateBranch, createOrUpdateBranch,
CreateOrUpdateBranchResult,
getWorkingBaseAndType, getWorkingBaseAndType,
WorkingBaseType WorkingBaseType
} from './create-or-update-branch' } from './create-or-update-branch'
import {GitHubHelper} from './github-helper' import {GitHubHelper} from './github-helper'
import {GitCommandManager} from './git-command-manager' import {GitCommandManager} from './git-command-manager'
import {GitConfigHelper} from './git-config-helper' import {GitConfigHelper, GitRemote} from './git-config-helper'
import * as utils from './utils' import * as utils from './utils'
export interface Inputs { export interface Inputs {
@ -39,119 +40,21 @@ export interface Inputs {
maintainerCanModify: boolean maintainerCanModify: boolean
} }
type Ouputs = Map<string, string>
export async function createPullRequest(inputs: Inputs): Promise<void> { export async function createPullRequest(inputs: Inputs): Promise<void> {
let gitConfigHelper, git let gitConfigHelper: GitConfigHelper | undefined,
git: GitCommandManager | undefined
try { try {
core.startGroup('Prepare git configuration') const {git, gitConfigHelper, repoPath} =
const repoPath = utils.getRepoPath(inputs.path) await prepareGitConfiguration(inputs)
git = await GitCommandManager.create(repoPath)
gitConfigHelper = await GitConfigHelper.create(git)
core.endGroup()
core.startGroup('Determining the base and head repositories') const {branchRemoteName, ghBranch, branchRepository, ghPull, baseRemote} =
const baseRemote = gitConfigHelper.getGitRemote() await initializePullRequestContext(inputs, gitConfigHelper, git)
// Init the GitHub clients
const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.branchToken)
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
? inputs.pushToFork
: baseRemote.repository
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 = await ghBranch.getRepositoryParent(
baseRemote.repository
)
const branchParentRepository =
await ghBranch.getRepositoryParent(branchRepository)
if (branchParentRepository == null) {
throw new Error(
`Repository '${branchRepository}' is not a fork. Unable to continue.`
)
}
if (
branchParentRepository != baseRemote.repository &&
baseParentRepository != branchParentRepository
) {
throw new Error(
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`
)
}
// Add a remote for the fork
const remoteUrl = utils.getRemoteUrl(
baseRemote.protocol,
baseRemote.hostname,
branchRepository
)
await git.exec(['remote', 'add', 'fork', remoteUrl])
}
core.endGroup()
core.info(
`Pull request branch target repository set to ${branchRepository}`
)
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication')
await gitConfigHelper.configureToken(inputs.branchToken)
core.endGroup()
}
core.startGroup('Checking the base repository state')
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
core.info(`Working base is ${workingBaseType} '${workingBase}'`)
// When in detached HEAD state (checked out on a commit), we need to
// know the 'base' branch in order to rebase changes.
if (workingBaseType == WorkingBaseType.Commit && !inputs.base) {
throw new Error(
`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`
)
}
// If the base is not specified it is assumed to be the working base.
const base = inputs.base ? inputs.base : workingBase
// Throw an error if the base and branch are not different branches
// of the 'origin' remote. An identically named branch in the `fork`
// remote is perfectly fine.
if (branchRemoteName == 'origin' && base == inputs.branch) {
throw new Error(
`The 'base' and 'branch' for a pull request must be different branches. Unable to continue.`
)
}
// For self-hosted runners the repository state persists between runs.
// This command prunes the stale remote ref when the pull request branch was
// deleted after being merged or closed. Without this the push using
// '--force-with-lease' fails due to "stale info."
// https://github.com/peter-evans/create-pull-request/issues/633
await git.exec(['remote', 'prune', branchRemoteName])
core.endGroup()
// Apply the branch suffix if set // Apply the branch suffix if set
if (inputs.branchSuffix) { await defineInputBranch(inputs, git)
switch (inputs.branchSuffix) {
case 'short-commit-hash':
// Suffix with the short SHA1 hash
inputs.branch = `${inputs.branch}-${await git.revParse('HEAD', [
'--short'
])}`
break
case 'timestamp':
// Suffix with the current timestamp
inputs.branch = `${inputs.branch}-${utils.secondsSinceEpoch()}`
break
case 'random':
// Suffix with a 7 character random string
inputs.branch = `${inputs.branch}-${utils.randomString()}`
break
default:
throw new Error(
`Branch suffix '${inputs.branchSuffix}' is not a valid value. Unable to continue.`
)
}
}
// Output head branch // Output head branch
core.info( core.info(
@ -159,166 +62,404 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
) )
// Configure the committer and author // Configure the committer and author
core.startGroup('Configuring the committer and author') configureGitIdentity(inputs, git)
const parsedAuthor = utils.parseDisplayNameEmail(inputs.author)
const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer)
git.setIdentityGitOptions([
'-c',
`author.name=${parsedAuthor.name}`,
'-c',
`author.email=${parsedAuthor.email}`,
'-c',
`committer.name=${parsedCommitter.name}`,
'-c',
`committer.email=${parsedCommitter.email}`
])
core.info(
`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`
)
core.info(
`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`
)
core.endGroup()
// Action outputs // Action outputs
const outputs = new Map<string, string>() const outputs: Ouputs = new Map<string, string>()
outputs.set('pull-request-branch', inputs.branch) outputs.set('pull-request-branch', inputs.branch)
outputs.set('pull-request-operation', 'none') outputs.set('pull-request-operation', 'none')
// Create or update the pull request branch // Create or update the pull request branch
core.startGroup('Create or update the pull request branch') const result = await createOrUpdatePullRequestBranch(
const result = await createOrUpdateBranch(
git, git,
inputs.commitMessage, inputs,
inputs.base, branchRemoteName
inputs.branch,
branchRemoteName,
inputs.signoff,
inputs.addPaths
) )
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()
if (['created', 'updated'].includes(result.action)) { await processPullRequestBranch(
// The branch was created or updated result,
core.startGroup( branchRemoteName,
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'` inputs,
) git,
if (inputs.signCommits) { ghBranch,
// Create signed commits via the GitHub API repoPath,
const stashed = await git.stashPush(['--include-untracked']) branchRepository,
await git.checkout(inputs.branch) outputs,
const pushSignedCommitsResult = await ghBranch.pushSignedCommits( ghPull,
git, baseRemote
result.branchCommits, )
result.baseCommit,
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()
}
} else {
await git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
])
}
core.endGroup()
}
if (result.hasDiffWithBase) { await exportOutputs(outputs, result, ghBranch, branchRepository)
core.startGroup('Create or update the pull request')
const pull = await 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) {
outputs.set('pull-request-operation', 'created')
} else if (result.action == 'updated') {
outputs.set('pull-request-operation', 'updated')
// The pull request was updated AND the branch was updated.
// Convert back to draft if 'draft: always-true' is set.
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
await ghPull.convertToDraft(pull.node_id)
}
}
core.endGroup()
} else {
// There is no longer a diff with the base
// Check we are in a state where a branch exists
if (['updated', 'not-updated'].includes(result.action)) {
core.info(
`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`
)
if (inputs.deleteBranch) {
core.info(`Deleting branch '${inputs.branch}'`)
await git.push([
'--delete',
'--force',
branchRemoteName,
`refs/heads/${inputs.branch}`
])
outputs.set('pull-request-operation', 'closed')
}
}
}
core.startGroup('Setting outputs')
// If the head commit is signed, get its verification status if we don't already know it.
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
if (
!outputs.has('pull-request-commits-verified') &&
result.branchCommits.length > 0 &&
result.branchCommits[result.branchCommits.length - 1].signed
) {
// Using the local head commit SHA because in this case commits have not been pushed via the API.
core.info(`Checking verification status of head commit ${result.headSha}`)
try {
const headCommit = await ghBranch.getCommit(
result.headSha,
branchRepository
)
outputs.set(
'pull-request-commits-verified',
headCommit.verified.toString()
)
} catch (error) {
core.warning('Failed to check verification status of head commit.')
core.debug(utils.getErrorMessage(error))
}
}
if (!outputs.has('pull-request-commits-verified')) {
outputs.set('pull-request-commits-verified', 'false')
}
// Set outputs
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`)
core.setOutput(key, value)
}
core.endGroup()
} catch (error) { } catch (error) {
core.setFailed(utils.getErrorMessage(error)) core.setFailed(utils.getErrorMessage(error))
} finally { } finally {
core.startGroup('Restore git configuration') core.startGroup('Restore git configuration')
if (inputs.pushToFork) { if (inputs.pushToFork) {
await git.exec(['remote', 'rm', 'fork']) await git?.exec(['remote', 'rm', 'fork'])
} }
await gitConfigHelper.close() await gitConfigHelper?.close()
core.endGroup() core.endGroup()
} }
} }
async function prepareGitConfiguration(inputs: Inputs): Promise<{
repoPath: string
git: GitCommandManager
gitConfigHelper: GitConfigHelper
}> {
core.startGroup('Prepare git configuration')
const repoPath = utils.getRepoPath(inputs.path)
const git = await GitCommandManager.create(repoPath)
const gitConfigHelper = await GitConfigHelper.create(git)
core.endGroup()
return {
repoPath,
git,
gitConfigHelper
}
}
async function initializePullRequestContext(
inputs: Inputs,
gitConfigHelper: GitConfigHelper,
git: GitCommandManager
): Promise<{
branchRemoteName: string
branchRepository: string
baseRemote: GitRemote
ghBranch: GitHubHelper
ghPull: GitHubHelper
}> {
core.startGroup('Determining the base and head repositories')
const baseRemote = gitConfigHelper.getGitRemote()
// Init the GitHub clients
const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.branchToken)
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
? inputs.pushToFork
: baseRemote.repository
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 = await ghBranch.getRepositoryParent(
baseRemote.repository
)
const branchParentRepository =
await ghBranch.getRepositoryParent(branchRepository)
if (branchParentRepository == null) {
throw new Error(
`Repository '${branchRepository}' is not a fork. Unable to continue.`
)
}
if (
branchParentRepository != baseRemote.repository &&
baseParentRepository != branchParentRepository
) {
throw new Error(
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`
)
}
// Add a remote for the fork
const remoteUrl = utils.getRemoteUrl(
baseRemote.protocol,
baseRemote.hostname,
branchRepository
)
await git.exec(['remote', 'add', 'fork', remoteUrl])
}
core.endGroup()
core.info(`Pull request branch target repository set to ${branchRepository}`)
// Configure auth
if (baseRemote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication')
await gitConfigHelper.configureToken(inputs.branchToken)
core.endGroup()
}
core.startGroup('Checking the base repository state')
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
core.info(`Working base is ${workingBaseType} '${workingBase}'`)
// When in detached HEAD state (checked out on a commit), we need to
// know the 'base' branch in order to rebase changes.
if (workingBaseType == WorkingBaseType.Commit && !inputs.base) {
throw new Error(
`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`
)
}
// If the base is not specified it is assumed to be the working base.
const base = inputs.base ? inputs.base : workingBase
// Throw an error if the base and branch are not different branches
// of the 'origin' remote. An identically named branch in the `fork`
// remote is perfectly fine.
if (branchRemoteName == 'origin' && base == inputs.branch) {
throw new Error(
`The 'base' and 'branch' for a pull request must be different branches. Unable to continue.`
)
}
// For self-hosted runners the repository state persists between runs.
// This command prunes the stale remote ref when the pull request branch was
// deleted after being merged or closed. Without this the push using
// '--force-with-lease' fails due to "stale info."
// https://github.com/peter-evans/create-pull-request/issues/633
await git.exec(['remote', 'prune', branchRemoteName])
core.endGroup()
return {branchRemoteName, branchRepository, baseRemote, ghBranch, ghPull}
}
async function defineInputBranch(inputs: Inputs, git: GitCommandManager) {
if (inputs.branchSuffix) {
switch (inputs.branchSuffix) {
case 'short-commit-hash':
// Suffix with the short SHA1 hash
inputs.branch = `${inputs.branch}-${await git.revParse('HEAD', [
'--short'
])}`
break
case 'timestamp':
// Suffix with the current timestamp
inputs.branch = `${inputs.branch}-${utils.secondsSinceEpoch()}`
break
case 'random':
// Suffix with a 7 character random string
inputs.branch = `${inputs.branch}-${utils.randomString()}`
break
default:
throw new Error(
`Branch suffix '${inputs.branchSuffix}' is not a valid value. Unable to continue.`
)
}
}
}
function configureGitIdentity(inputs: Inputs, git: GitCommandManager) {
core.startGroup('Configuring the committer and author')
const parsedAuthor = utils.parseDisplayNameEmail(inputs.author)
const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer)
git.setIdentityGitOptions([
'-c',
`author.name=${parsedAuthor.name}`,
'-c',
`author.email=${parsedAuthor.email}`,
'-c',
`committer.name=${parsedCommitter.name}`,
'-c',
`committer.email=${parsedCommitter.email}`
])
core.info(
`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`
)
core.info(
`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`
)
core.endGroup()
}
async function createOrUpdatePullRequestBranch(
git: GitCommandManager,
inputs: Inputs,
branchRemoteName: string
) {
core.startGroup('Create or update the pull request branch')
const result = await createOrUpdateBranch(
git,
inputs.commitMessage,
inputs.base,
inputs.branch,
branchRemoteName,
inputs.signoff,
inputs.addPaths
)
core.endGroup()
return result
}
async function processPullRequestBranch(
result: CreateOrUpdateBranchResult,
branchRemoteName: string,
inputs: Inputs,
git: GitCommandManager,
ghBranch: GitHubHelper,
repoPath: string,
branchRepository: any,
outputs: Ouputs,
ghPull: GitHubHelper,
baseRemote: GitRemote
) {
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
if (['created', 'updated'].includes(result.action)) {
// The branch was created or updated
await pushPullRequestBranch(
branchRemoteName,
inputs,
git,
ghBranch,
result,
repoPath,
branchRepository,
outputs
)
}
if (result.hasDiffWithBase) {
await createOrUpdatePullRequest(
ghPull,
inputs,
baseRemote,
branchRepository,
outputs,
result
)
} else {
// There is no longer a diff with the base
// Check we are in a state where a branch exists
if (['updated', 'not-updated'].includes(result.action)) {
core.info(
`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`
)
if (inputs.deleteBranch) {
core.info(`Deleting branch '${inputs.branch}'`)
await git.push([
'--delete',
'--force',
branchRemoteName,
`refs/heads/${inputs.branch}`
])
outputs.set('pull-request-operation', 'closed')
}
}
}
}
async function createOrUpdatePullRequest(
ghPull: GitHubHelper,
inputs: Inputs,
baseRemote: GitRemote,
branchRepository: any,
outputs: Ouputs,
result: CreateOrUpdateBranchResult
) {
core.startGroup('Create or update the pull request')
const pull = await 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) {
outputs.set('pull-request-operation', 'created')
} else if (result.action == 'updated') {
outputs.set('pull-request-operation', 'updated')
// The pull request was updated AND the branch was updated.
// Convert back to draft if 'draft: always-true' is set.
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
await ghPull.convertToDraft(pull.node_id)
}
}
core.endGroup()
}
async function pushPullRequestBranch(
branchRemoteName: string,
inputs: Inputs,
git: GitCommandManager,
ghBranch: GitHubHelper,
result: CreateOrUpdateBranchResult,
repoPath: string,
branchRepository: any,
outputs: Ouputs
) {
core.startGroup(
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
)
if (inputs.signCommits) {
// Create signed commits via the GitHub API
const stashed = await git.stashPush(['--include-untracked'])
await git.checkout(inputs.branch)
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
git,
result.branchCommits,
result.baseCommit,
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()
}
} else {
await git.push([
'--force-with-lease',
branchRemoteName,
`${inputs.branch}:refs/heads/${inputs.branch}`
])
}
core.endGroup()
}
async function exportOutputs(
outputs: Ouputs,
result: CreateOrUpdateBranchResult,
ghBranch: GitHubHelper,
branchRepository: string
) {
core.startGroup('Setting outputs')
// If the head commit is signed, get its verification status if we don't already know it.
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
if (
!outputs.has('pull-request-commits-verified') &&
result.branchCommits.length > 0 &&
result.branchCommits[result.branchCommits.length - 1].signed
) {
// Using the local head commit SHA because in this case commits have not been pushed via the API.
core.info(`Checking verification status of head commit ${result.headSha}`)
try {
const headCommit = await ghBranch.getCommit(
result.headSha,
branchRepository
)
outputs.set(
'pull-request-commits-verified',
headCommit.verified.toString()
)
} catch (error) {
core.warning('Failed to check verification status of head commit.')
core.debug(utils.getErrorMessage(error))
}
}
if (!outputs.has('pull-request-commits-verified')) {
outputs.set('pull-request-commits-verified', 'false')
}
// Set outputs
for (const [key, value] of outputs) {
core.info(`${key} = ${value}`)
core.setOutput(key, value)
}
core.endGroup()
}

View File

@ -5,7 +5,7 @@ import * as path from 'path'
import {URL} from 'url' import {URL} from 'url'
import * as utils from './utils' import * as utils from './utils'
interface GitRemote { export interface GitRemote {
hostname: string hostname: string
protocol: string protocol: string
repository: string repository: string