fix(workflow): prevent PR overwrite when branch modified by others

This commit is contained in:
BLACKBOX Agent 2025-11-06 17:07:17 +00:00
parent 0edc001d28
commit 076b9948be
6 changed files with 167 additions and 7 deletions

View File

@ -82,6 +82,11 @@ inputs:
maintainer-can-modify:
description: 'Indicates whether maintainers can modify the pull request.'
default: true
skip-if-commits-from-other-authors:
description: >
Skip updating the pull request branch if it contains commits from authors other than the configured action author/committer.
This prevents overwriting changes made by other users to the branch.
default: false
outputs:
pull-request-number:
description: 'The pull request number'

71
dist/index.js vendored
View File

@ -168,8 +168,8 @@ function splitLines(multilineString) {
.map(s => s.trim())
.filter(x => x !== '');
}
function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName, signoff, addPaths) {
return __awaiter(this, void 0, void 0, function* () {
function createOrUpdateBranch(git_1, commitMessage_1, base_1, branch_1, branchRemoteName_1, signoff_1, addPaths_1) {
return __awaiter(this, arguments, void 0, function* (git, commitMessage, base, branch, branchRemoteName, signoff, addPaths, skipIfCommitsFromOtherAuthors = false, authorEmail = '', committerEmail = '') {
// Get the working base.
// When a ref, it may or may not be the actual base.
// When a commit, we must rebase onto the actual base.
@ -268,6 +268,52 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName
core.info(`Pull request branch '${branch}' already exists as remote branch '${branchRemoteName}/${branch}'`);
// Checkout the pull request branch
yield git.checkout(branch);
// Check if the branch has commits from other authors
if (skipIfCommitsFromOtherAuthors && authorEmail && committerEmail) {
core.info('Checking if branch has commits from other authors...');
const branchCommitsAheadCount = yield commitsAhead(git, base, branch);
if (branchCommitsAheadCount > 0) {
try {
const commitAuthors = yield git.getCommitAuthors(`${base}..${branch}`);
const hasOtherAuthors = commitAuthors.some(commit => commit.authorEmail !== authorEmail &&
commit.committerEmail !== committerEmail);
if (hasOtherAuthors) {
core.info(`Branch '${branch}' has commits from other authors. Skipping update to prevent overwriting their changes.`);
core.info(`Configured author: ${authorEmail}, committer: ${committerEmail}`);
const otherAuthors = commitAuthors.filter(commit => commit.authorEmail !== authorEmail ||
commit.committerEmail !== committerEmail);
core.info(`Found commits from: ${otherAuthors.map(c => `${c.authorEmail} (committer: ${c.committerEmail})`).join(', ')}`);
action = 'not-updated';
hasDiffWithBase = yield isAhead(git, base, branch);
const baseSha = yield git.revParse(base);
const baseCommit = yield git.getCommit(baseSha);
const headSha = yield git.revParse(branch);
let branchCommits = [];
if (hasDiffWithBase) {
branchCommits = yield buildBranchCommits(git, base, branch);
}
yield git.exec(['branch', '--delete', '--force', tempBranch]);
yield git.checkout(workingBase);
if (stashed) {
yield git.stashPop();
}
return {
action: action,
base: base,
hasDiffWithBase: hasDiffWithBase,
baseCommit: baseCommit,
headSha: headSha,
branchCommits: branchCommits
};
}
core.info('No commits from other authors found. Proceeding with update.');
}
catch (error) {
core.warning(`Failed to check commit authors: ${utils.getErrorMessage(error)}`);
core.info('Proceeding with update despite check failure.');
}
}
}
// Reset the branch if one of the following conditions is true.
// - If the branch differs from the recreated temp branch.
// - If the number of commits ahead of the base branch differs between the branch and
@ -505,7 +551,7 @@ function createPullRequest(inputs) {
outputs.set('pull-request-operation', 'none');
// Create or update the pull request branch
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);
const result = yield (0, create_or_update_branch_1.createOrUpdateBranch)(git, inputs.commitMessage, inputs.base, inputs.branch, branchRemoteName, inputs.signoff, inputs.addPaths, inputs.skipIfCommitsFromOtherAuthors, parsedAuthor.email, parsedCommitter.email);
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;
@ -815,6 +861,22 @@ class GitCommandManager {
};
});
}
getCommitAuthors(commitRange) {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.exec(['log', '--format=%ae%n%ce%n###COMMIT###', commitRange], { suppressGitCmdOutput: true });
const commits = [];
const lines = output.stdout.split('\n').filter(x => x !== '');
for (let i = 0; i < lines.length; i += 3) {
if (lines[i + 2] === '###COMMIT###') {
commits.push({
authorEmail: lines[i],
committerEmail: lines[i + 1]
});
}
}
return commits;
});
}
getConfigValue(configKey_1) {
return __awaiter(this, arguments, void 0, function* (configKey, configValue = '.') {
const output = yield this.exec([
@ -1654,7 +1716,8 @@ function run() {
teamReviewers: utils.getInputAsArray('team-reviewers'),
milestone: Number(core.getInput('milestone')),
draft: getDraftInput(),
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
maintainerCanModify: core.getBooleanInput('maintainer-can-modify'),
skipIfCommitsFromOtherAuthors: core.getBooleanInput('skip-if-commits-from-other-authors')
};
core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`);
if (!inputs.token) {

View File

@ -173,7 +173,10 @@ export async function createOrUpdateBranch(
branch: string,
branchRemoteName: string,
signoff: boolean,
addPaths: string[]
addPaths: string[],
skipIfCommitsFromOtherAuthors = false,
authorEmail = '',
committerEmail = ''
): Promise<CreateOrUpdateBranchResult> {
// Get the working base.
// When a ref, it may or may not be the actual base.
@ -294,6 +297,68 @@ export async function createOrUpdateBranch(
// Checkout the pull request branch
await git.checkout(branch)
// Check if the branch has commits from other authors
if (skipIfCommitsFromOtherAuthors && authorEmail && committerEmail) {
core.info('Checking if branch has commits from other authors...')
const branchCommitsAheadCount = await commitsAhead(git, base, branch)
if (branchCommitsAheadCount > 0) {
try {
const commitAuthors = await git.getCommitAuthors(`${base}..${branch}`)
const hasOtherAuthors = commitAuthors.some(
commit =>
commit.authorEmail !== authorEmail &&
commit.committerEmail !== committerEmail
)
if (hasOtherAuthors) {
core.info(
`Branch '${branch}' has commits from other authors. Skipping update to prevent overwriting their changes.`
)
core.info(
`Configured author: ${authorEmail}, committer: ${committerEmail}`
)
const otherAuthors = commitAuthors.filter(
commit =>
commit.authorEmail !== authorEmail ||
commit.committerEmail !== committerEmail
)
core.info(
`Found commits from: ${otherAuthors.map(c => `${c.authorEmail} (committer: ${c.committerEmail})`).join(', ')}`
)
action = 'not-updated'
hasDiffWithBase = await isAhead(git, base, branch)
const baseSha = await git.revParse(base)
const baseCommit = await git.getCommit(baseSha)
const headSha = await git.revParse(branch)
let branchCommits: Commit[] = []
if (hasDiffWithBase) {
branchCommits = await buildBranchCommits(git, base, branch)
}
await git.exec(['branch', '--delete', '--force', tempBranch])
await git.checkout(workingBase)
if (stashed) {
await git.stashPop()
}
return {
action: action,
base: base,
hasDiffWithBase: hasDiffWithBase,
baseCommit: baseCommit,
headSha: headSha,
branchCommits: branchCommits
}
}
core.info(
'No commits from other authors found. Proceeding with update.'
)
} catch (error) {
core.warning(
`Failed to check commit authors: ${utils.getErrorMessage(error)}`
)
core.info('Proceeding with update despite check failure.')
}
}
}
// Reset the branch if one of the following conditions is true.
// - If the branch differs from the recreated temp branch.
// - If the number of commits ahead of the base branch differs between the branch and

View File

@ -37,6 +37,7 @@ export interface Inputs {
always: boolean
}
maintainerCanModify: boolean
skipIfCommitsFromOtherAuthors: boolean
}
export async function createPullRequest(inputs: Inputs): Promise<void> {
@ -194,7 +195,10 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
inputs.branch,
branchRemoteName,
inputs.signoff,
inputs.addPaths
inputs.addPaths,
inputs.skipIfCommitsFromOtherAuthors,
parsedAuthor.email,
parsedCommitter.email
)
outputs.set('pull-request-head-sha', result.headSha)
// Set the base. It would have been '' if not specified as an input

View File

@ -208,6 +208,26 @@ export class GitCommandManager {
}
}
async getCommitAuthors(
commitRange: string
): Promise<{authorEmail: string; committerEmail: string}[]> {
const output = await this.exec(
['log', '--format=%ae%n%ce%n###COMMIT###', commitRange],
{suppressGitCmdOutput: true}
)
const commits: {authorEmail: string; committerEmail: string}[] = []
const lines = output.stdout.split('\n').filter(x => x !== '')
for (let i = 0; i < lines.length; i += 3) {
if (lines[i + 2] === '###COMMIT###') {
commits.push({
authorEmail: lines[i],
committerEmail: lines[i + 1]
})
}
}
return commits
}
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
const output = await this.exec([
'config',

View File

@ -37,7 +37,10 @@ async function run(): Promise<void> {
teamReviewers: utils.getInputAsArray('team-reviewers'),
milestone: Number(core.getInput('milestone')),
draft: getDraftInput(),
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
maintainerCanModify: core.getBooleanInput('maintainer-can-modify'),
skipIfCommitsFromOtherAuthors: core.getBooleanInput(
'skip-if-commits-from-other-authors'
)
}
core.debug(`Inputs: ${inspect(inputs)}`)